first commit
This commit is contained in:
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/list_pelanggan_aktif_controller.dart';
|
||||
|
||||
class ListPelangganAktifBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<ListPelangganAktifController>(
|
||||
() => ListPelangganAktifController(),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/list_petugas_mitra_controller.dart';
|
||||
|
||||
class ListPetugasMitraBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<ListPetugasMitraController>(() => ListPetugasMitraController());
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/list_tagihan_periode_controller.dart';
|
||||
|
||||
class ListTagihanPeriodeBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<ListTagihanPeriodeController>(
|
||||
() => ListTagihanPeriodeController(),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_aset_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
|
||||
class PetugasAsetBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Ensure dashboard controller is registered
|
||||
if (!Get.isRegistered<PetugasBumdesDashboardController>()) {
|
||||
Get.put(PetugasBumdesDashboardController(), permanent: true);
|
||||
}
|
||||
|
||||
Get.lazyPut<PetugasAsetController>(() => PetugasAsetController());
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_bumdes_cbp_controller.dart';
|
||||
|
||||
class PetugasBumdesCbpBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<PetugasBumdesCbpController>(() => PetugasBumdesCbpController());
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_sewa_controller.dart';
|
||||
|
||||
class PetugasDetailSewaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Memastikan controller sudah tersedia
|
||||
Get.lazyPut<PetugasSewaController>(
|
||||
() => PetugasSewaController(),
|
||||
fenix: true,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_manajemen_bumdes_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
|
||||
class PetugasManajemenBumdesBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Make sure AuthProvider is registered
|
||||
if (!Get.isRegistered<AuthProvider>()) {
|
||||
Get.put(AuthProvider());
|
||||
}
|
||||
|
||||
// Register the dashboard controller if not already registered
|
||||
if (!Get.isRegistered<PetugasBumdesDashboardController>()) {
|
||||
Get.put<PetugasBumdesDashboardController>(
|
||||
PetugasBumdesDashboardController(),
|
||||
permanent: true,
|
||||
);
|
||||
}
|
||||
|
||||
// Register the manajemen bumdes controller
|
||||
Get.lazyPut<PetugasManajemenBumdesController>(
|
||||
() => PetugasManajemenBumdesController(),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_paket_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
|
||||
class PetugasPaketBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Ensure dashboard controller is registered
|
||||
if (!Get.isRegistered<PetugasBumdesDashboardController>()) {
|
||||
Get.put(PetugasBumdesDashboardController(), permanent: true);
|
||||
}
|
||||
|
||||
Get.lazyPut<PetugasPaketController>(() => PetugasPaketController());
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_sewa_controller.dart';
|
||||
|
||||
class PetugasSewaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<PetugasSewaController>(() => PetugasSewaController());
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_tambah_aset_controller.dart';
|
||||
|
||||
class PetugasTambahAsetBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<PetugasTambahAsetController>(
|
||||
() => PetugasTambahAsetController(),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_tambah_paket_controller.dart';
|
||||
|
||||
class PetugasTambahPaketBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<PetugasTambahPaketController>(
|
||||
() => PetugasTambahPaketController(),
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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']}';
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,581 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/list_pelanggan_aktif_controller.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class ListPelangganAktifView extends GetView<ListPelangganAktifController> {
|
||||
const ListPelangganAktifView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get dashboard controller for navigation
|
||||
final dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
appBar: AppBar(
|
||||
title: Obx(
|
||||
() => Text(
|
||||
'Pelanggan ${controller.serviceName.value}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
actions: [
|
||||
// Actions removed
|
||||
],
|
||||
),
|
||||
drawer: PetugasSideNavbar(controller: dashboardController),
|
||||
drawerEdgeDragWidth: 60,
|
||||
drawerScrimColor: Colors.black.withOpacity(0.6),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
_buildSearchBar(),
|
||||
Expanded(child: _buildSubscribersList()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
offset: const Offset(0, 2),
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(
|
||||
() => Text(
|
||||
'Pelanggan Aktif ${controller.serviceName.value}',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Obx(
|
||||
() => Text(
|
||||
'Daftar warga yang berlangganan ${controller.serviceName.value.toLowerCase()}',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.people_alt_rounded,
|
||||
size: 16,
|
||||
color: Colors.green,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Obx(
|
||||
() => Text(
|
||||
'${controller.pelangganList.length} Pelanggan Aktif',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: Colors.white,
|
||||
child: TextField(
|
||||
onChanged: controller.updateSearchQuery,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari pelanggan...',
|
||||
prefixIcon: const Icon(Icons.search, color: Colors.grey),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubscribersList() {
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.filteredPelangganList.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredPelangganList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final pelanggan = controller.filteredPelangganList[index];
|
||||
return _buildPelangganCard(pelanggan);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPelangganCard(Map<String, dynamic> pelanggan) {
|
||||
final statusColor = Color(controller.getStatusColor(pelanggan['status']));
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
onTap:
|
||||
() => Get.toNamed(
|
||||
Routes.LIST_TAGIHAN_PERIODE,
|
||||
arguments: {
|
||||
'pelanggan': pelanggan,
|
||||
'serviceName': controller.serviceName.value,
|
||||
},
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundColor: AppColorsPetugas.babyBlueBright,
|
||||
child: Text(
|
||||
pelanggan['nama'].substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
pelanggan['nama'],
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
pelanggan['alamat'],
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, size: 14, color: statusColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
pelanggan['status'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildInfoItem(
|
||||
icon: Icons.calendar_month,
|
||||
label: 'Mulai',
|
||||
value: pelanggan['tanggal_mulai'],
|
||||
),
|
||||
_buildInfoItem(
|
||||
icon: Icons.payment,
|
||||
label: 'Tagihan',
|
||||
value: pelanggan['tagihan'],
|
||||
),
|
||||
_buildInfoItem(
|
||||
icon: Icons.phone,
|
||||
label: 'Telepon',
|
||||
value: pelanggan['telepon'],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoItem({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
}) {
|
||||
return Column(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: Colors.grey.shade600),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.people_alt_outlined,
|
||||
size: 60,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Tidak ada pelanggan aktif',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Belum ada warga yang berlangganan layanan ini',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPelangganDetails(Map<String, dynamic> pelanggan) {
|
||||
final statusColor = Color(controller.getStatusColor(pelanggan['status']));
|
||||
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundColor: Colors.white,
|
||||
child: Text(
|
||||
pelanggan['nama'].substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
pelanggan['nama'],
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
pelanggan['alamat'],
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
size: 14,
|
||||
color: statusColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
pelanggan['status'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Detail Pelanggan',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildDetailRow(
|
||||
icon: Icons.calendar_month,
|
||||
label: 'Tanggal Mulai',
|
||||
value: pelanggan['tanggal_mulai'],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.event_busy,
|
||||
label: 'Tanggal Berakhir',
|
||||
value: pelanggan['tanggal_berakhir'],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.payment,
|
||||
label: 'Pembayaran Terakhir',
|
||||
value: pelanggan['pembayaran_terakhir'],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.receipt_long,
|
||||
label: 'Tagihan',
|
||||
value: pelanggan['tagihan'],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Kontak',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildDetailRow(
|
||||
icon: Icons.phone,
|
||||
label: 'Telepon',
|
||||
value: pelanggan['telepon'],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.email,
|
||||
label: 'Email',
|
||||
value: pelanggan['email'],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (pelanggan['catatan'] != null) ...[
|
||||
const Text(
|
||||
'Catatan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
pelanggan['catatan'],
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.close),
|
||||
label: const Text('Tutup'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, size: 16, color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,720 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/list_petugas_mitra_controller.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
|
||||
class ListPetugasMitraView extends GetView<ListPetugasMitraController> {
|
||||
const ListPetugasMitraView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Petugas Mitra',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help_outline),
|
||||
onPressed: () {
|
||||
_showHelpDialog(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Search Bar
|
||||
_buildSearchBar(),
|
||||
|
||||
// List of Partners
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.filteredPartners.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return _buildPartnersList();
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_showAddPartnerDialog(context);
|
||||
},
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: Colors.white,
|
||||
child: TextField(
|
||||
onChanged: (value) {
|
||||
controller.searchQuery.value = value;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari petugas mitra...',
|
||||
prefixIcon: const Icon(Icons.search, color: Colors.grey),
|
||||
suffixIcon: Obx(() {
|
||||
return controller.searchQuery.value.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
controller.searchQuery.value = '';
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.people_outline, size: 80, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Belum ada petugas mitra',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tambahkan petugas mitra dengan menekan tombol "+" di bawah',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPartnersList() {
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredPartners.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final partner = controller.filteredPartners[index];
|
||||
return _buildPartnerCard(partner);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPartnerCard(Map<String, dynamic> partner) {
|
||||
final isActive = partner['is_active'] as bool;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor:
|
||||
isActive ? Colors.green.shade100 : Colors.red.shade100,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color:
|
||||
isActive ? Colors.green.shade700 : Colors.red.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
partner['name'],
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
partner['role'],
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? Colors.green.shade50 : Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
isActive ? 'Aktif' : 'Nonaktif',
|
||||
style: TextStyle(
|
||||
color:
|
||||
isActive
|
||||
? Colors.green.shade700
|
||||
: Colors.red.shade700,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (value) {
|
||||
_handleMenuAction(value, partner);
|
||||
},
|
||||
itemBuilder:
|
||||
(BuildContext context) => [
|
||||
PopupMenuItem<String>(
|
||||
value: 'toggle_status',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isActive ? Icons.toggle_off : Icons.toggle_on,
|
||||
color:
|
||||
isActive
|
||||
? Colors.red.shade700
|
||||
: Colors.green.shade700,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(isActive ? 'Nonaktifkan' : 'Aktifkan'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.edit, color: Colors.blue, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('Edit'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete, color: Colors.red, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('Hapus'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoRow(Icons.phone, 'Kontak', partner['contact']),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow(Icons.location_on, 'Alamat', partner['address']),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow(
|
||||
Icons.calendar_today,
|
||||
'Tanggal Bergabung',
|
||||
partner['join_date'],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(IconData icon, String label, String value) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$label:',
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey.shade700),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _handleMenuAction(String action, Map<String, dynamic> partner) {
|
||||
switch (action) {
|
||||
case 'toggle_status':
|
||||
controller.togglePartnerStatus(partner['id']);
|
||||
break;
|
||||
case 'edit':
|
||||
_showEditPartnerDialog(Get.context!, partner);
|
||||
break;
|
||||
case 'delete':
|
||||
_showDeleteConfirmationDialog(Get.context!, partner);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _showHelpDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Bantuan Petugas Mitra',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildHelpItem(
|
||||
Icons.add_circle_outline,
|
||||
'Tambah Mitra',
|
||||
'Tekan tombol + untuk menambah petugas mitra baru',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildHelpItem(
|
||||
Icons.toggle_on,
|
||||
'Aktif/Nonaktif',
|
||||
'Ubah status aktif petugas mitra melalui menu opsi',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildHelpItem(
|
||||
Icons.edit,
|
||||
'Edit Data',
|
||||
'Ubah informasi petugas mitra melalui menu opsi',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildHelpItem(
|
||||
Icons.delete,
|
||||
'Hapus',
|
||||
'Hapus petugas mitra melalui menu opsi',
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Mengerti'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHelpItem(IconData icon, String title, String description) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, color: AppColorsPetugas.blueGrotto, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddPartnerDialog(BuildContext context) {
|
||||
final nameController = TextEditingController();
|
||||
final contactController = TextEditingController();
|
||||
final addressController = TextEditingController();
|
||||
final roleController = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Tambah Petugas Mitra',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildTextField(nameController, 'Nama Lengkap', Icons.person),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
contactController,
|
||||
'Nomor Kontak',
|
||||
Icons.phone,
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
addressController,
|
||||
'Alamat',
|
||||
Icons.location_on,
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(roleController, 'Jabatan', Icons.work),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: AppColorsPetugas.navyBlue),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Batal',
|
||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (nameController.text.isEmpty ||
|
||||
contactController.text.isEmpty ||
|
||||
addressController.text.isEmpty ||
|
||||
roleController.text.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Harap isi semua data',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final newPartner = {
|
||||
'id':
|
||||
DateTime.now().millisecondsSinceEpoch
|
||||
.toString(),
|
||||
'name': nameController.text,
|
||||
'contact': contactController.text,
|
||||
'address': addressController.text,
|
||||
'role': roleController.text,
|
||||
'is_active': true,
|
||||
'join_date':
|
||||
'${DateTime.now().day} ${_getMonthName(DateTime.now().month)} ${DateTime.now().year}',
|
||||
};
|
||||
|
||||
controller.addPartner(newPartner);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditPartnerDialog(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> partner,
|
||||
) {
|
||||
final nameController = TextEditingController(text: partner['name']);
|
||||
final contactController = TextEditingController(text: partner['contact']);
|
||||
final addressController = TextEditingController(text: partner['address']);
|
||||
final roleController = TextEditingController(text: partner['role']);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Edit Petugas Mitra',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildTextField(nameController, 'Nama Lengkap', Icons.person),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
contactController,
|
||||
'Nomor Kontak',
|
||||
Icons.phone,
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
addressController,
|
||||
'Alamat',
|
||||
Icons.location_on,
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(roleController, 'Jabatan', Icons.work),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: AppColorsPetugas.navyBlue),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Batal',
|
||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (nameController.text.isEmpty ||
|
||||
contactController.text.isEmpty ||
|
||||
addressController.text.isEmpty ||
|
||||
roleController.text.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Harap isi semua data',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final updatedPartner = {
|
||||
'id': partner['id'],
|
||||
'name': nameController.text,
|
||||
'contact': contactController.text,
|
||||
'address': addressController.text,
|
||||
'role': roleController.text,
|
||||
'is_active': partner['is_active'],
|
||||
'join_date': partner['join_date'],
|
||||
};
|
||||
|
||||
controller.editPartner(
|
||||
partner['id'],
|
||||
updatedPartner,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmationDialog(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> partner,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Konfirmasi Penghapusan'),
|
||||
content: Text(
|
||||
'Apakah Anda yakin ingin menghapus petugas mitra "${partner['name']}"?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
controller.deletePartner(partner['id']);
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField(
|
||||
TextEditingController controller,
|
||||
String label,
|
||||
IconData icon, {
|
||||
TextInputType? keyboardType,
|
||||
int maxLines = 1,
|
||||
}) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
prefixIcon: Icon(icon),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getMonthName(int month) {
|
||||
const months = [
|
||||
'',
|
||||
'Januari',
|
||||
'Februari',
|
||||
'Maret',
|
||||
'April',
|
||||
'Mei',
|
||||
'Juni',
|
||||
'Juli',
|
||||
'Agustus',
|
||||
'September',
|
||||
'Oktober',
|
||||
'November',
|
||||
'Desember',
|
||||
];
|
||||
return months[month];
|
||||
}
|
||||
}
|
@ -0,0 +1,691 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/list_tagihan_periode_controller.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class ListTagihanPeriodeView extends GetView<ListTagihanPeriodeController> {
|
||||
const ListTagihanPeriodeView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get dashboard controller for navigation
|
||||
final dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'Riwayat Tagihan',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
drawer: PetugasSideNavbar(controller: dashboardController),
|
||||
drawerEdgeDragWidth: 60,
|
||||
drawerScrimColor: Colors.black.withOpacity(0.6),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [_buildHeader(), Expanded(child: _buildPeriodeList())],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
offset: const Offset(0, 2),
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(() {
|
||||
final pelanggan = controller.pelangganData.value;
|
||||
final nama = pelanggan['nama'] ?? 'Pelanggan';
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: AppColorsPetugas.babyBlueBright,
|
||||
child: Text(
|
||||
nama.substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
nama,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Obx(
|
||||
() => Text(
|
||||
'Pelanggan ${controller.serviceName.value}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
size: 14,
|
||||
color: Colors.green,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Aktif',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Riwayat Tagihan Bulanan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Daftar periode tagihan dan status pembayaran',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPeriodeList() {
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.filteredPeriodeList.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredPeriodeList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final periode = controller.filteredPeriodeList[index];
|
||||
return _buildPeriodeCard(periode);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPeriodeCard(Map<String, dynamic> periode) {
|
||||
final statusColor = Color(
|
||||
controller.getStatusColor(periode['status_pembayaran']),
|
||||
);
|
||||
final isCurrent = periode['is_current'] ?? false;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
border:
|
||||
isCurrent
|
||||
? Border.all(color: AppColorsPetugas.blueGrotto, width: 2)
|
||||
: null,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Get.snackbar(
|
||||
'Informasi',
|
||||
'Detail tagihan untuk periode ini tidak tersedia',
|
||||
backgroundColor: Colors.orange.withOpacity(0.1),
|
||||
colorText: Colors.orange.shade800,
|
||||
duration: const Duration(seconds: 3),
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(8),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isCurrent
|
||||
? AppColorsPetugas.babyBlueBright.withOpacity(0.3)
|
||||
: Colors.white,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
periode['bulan'].substring(0, 3),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
periode['tahun'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Periode ${controller.getPeriodeString(periode)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Jatuh tempo: 20 ${periode['bulan']} ${periode['tahun']}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
periode['status_pembayaran'].toLowerCase() == 'lunas'
|
||||
? Icons.check_circle
|
||||
: Icons.pending,
|
||||
size: 14,
|
||||
color: statusColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
periode['status_pembayaran'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Nominal',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
periode['nominal'],
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (periode['status_pembayaran'].toLowerCase() == 'lunas')
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'Tanggal Bayar',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
periode['tanggal_pembayaran'],
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: Icon(
|
||||
Icons.payment,
|
||||
size: 16,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
label: Text(
|
||||
'Bayar Sekarang',
|
||||
style: TextStyle(color: AppColorsPetugas.blueGrotto),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: BorderSide(color: AppColorsPetugas.blueGrotto),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isCurrent)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright.withOpacity(0.3),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Periode Berjalan',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.receipt_long, size: 60, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Tidak ada riwayat tagihan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Pelanggan belum memiliki riwayat tagihan',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPeriodeDetails(Map<String, dynamic> periode) {
|
||||
final statusColor = Color(
|
||||
controller.getStatusColor(periode['status_pembayaran']),
|
||||
);
|
||||
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
periode['bulan'].substring(0, 3),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
periode['tahun'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Detail Tagihan',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Periode ${controller.getPeriodeString(periode)}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
periode['status_pembayaran'].toLowerCase() ==
|
||||
'lunas'
|
||||
? Icons.check_circle
|
||||
: Icons.pending,
|
||||
size: 14,
|
||||
color: statusColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
periode['status_pembayaran'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Informasi Tagihan',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildDetailRow(
|
||||
icon: Icons.person,
|
||||
label: 'Pelanggan',
|
||||
value:
|
||||
controller.pelangganData.value['nama'] ?? 'Pelanggan',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.calendar_today,
|
||||
label: 'Periode',
|
||||
value: controller.getPeriodeString(periode),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.attach_money,
|
||||
label: 'Nominal',
|
||||
value: periode['nominal'],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.event,
|
||||
label: 'Jatuh Tempo',
|
||||
value: '20 ${periode['bulan']} ${periode['tahun']}',
|
||||
),
|
||||
if (periode['status_pembayaran'].toLowerCase() ==
|
||||
'lunas') ...[
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Informasi Pembayaran',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildDetailRow(
|
||||
icon: Icons.date_range,
|
||||
label: 'Tanggal Pembayaran',
|
||||
value: periode['tanggal_pembayaran'],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.payment,
|
||||
label: 'Metode Pembayaran',
|
||||
value: periode['metode_pembayaran'],
|
||||
),
|
||||
if (periode['keterangan'] != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.info_outline,
|
||||
label: 'Keterangan',
|
||||
value: periode['keterangan'],
|
||||
),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.close),
|
||||
label: const Text('Tutup'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, size: 16, color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
1291
lib/app/modules/petugas_bumdes/views/petugas_aset_view.dart
Normal file
1291
lib/app/modules/petugas_bumdes/views/petugas_aset_view.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,518 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_bumdes_cbp_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class PetugasBumdesCbpView extends GetView<PetugasBumdesCbpController> {
|
||||
const PetugasBumdesCbpView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'BUMDes CBP',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
),
|
||||
drawer: _buildDrawer(),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Text
|
||||
const Text(
|
||||
'Pengelolaan BUMDes CBP',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF333333),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Kelola informasi akun bank dan petugas mitra BUMDes',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Main Content
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
// Bank Account Card
|
||||
_buildInfoCard(
|
||||
title: 'Rekening Bank',
|
||||
icon: Icons.account_balance_outlined,
|
||||
primaryInfo:
|
||||
'${controller.bankAccounts.length} Rekening Terdaftar',
|
||||
secondaryInfo:
|
||||
controller.bankAccounts.isNotEmpty
|
||||
? 'Rekening Utama: ${controller.bankAccounts.firstWhere((acc) => acc['is_primary'] == true, orElse: () => {'bank_name': 'Tidak ada'})['bank_name']}'
|
||||
: 'Belum ada rekening utama',
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF0072B5), Color(0xFF0088CC)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
onTap: _showBankAccountsPage,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Partners Card
|
||||
_buildInfoCard(
|
||||
title: 'Petugas Mitra',
|
||||
icon: Icons.people_outline_rounded,
|
||||
primaryInfo: '${controller.partners.length} Mitra',
|
||||
secondaryInfo:
|
||||
'${controller.partners.where((p) => p['is_active'] == true).length} Mitra Aktif',
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF00B4D8), Color(0xFF48CAE4)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
onTap: _showPartnersPage,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: _buildBottomNavigationBar(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoCard({
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required String primaryInfo,
|
||||
required String secondaryInfo,
|
||||
required Gradient gradient,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: gradient,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: gradient.colors.first.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: Colors.white, size: 30),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
primaryInfo,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
secondaryInfo,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.85),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Lihat Detail',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Icon(Icons.arrow_forward, color: Colors.white, size: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomNavigationBar() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(0),
|
||||
topRight: Radius.circular(0),
|
||||
),
|
||||
child: BottomNavigationBar(
|
||||
currentIndex: 5, // BUMDes tab
|
||||
type: BottomNavigationBarType.fixed,
|
||||
backgroundColor: Colors.white,
|
||||
selectedItemColor: AppColorsPetugas.blueGrotto,
|
||||
unselectedItemColor: Colors.grey,
|
||||
selectedLabelStyle: const TextStyle(fontSize: 12),
|
||||
unselectedLabelStyle: const TextStyle(fontSize: 12),
|
||||
onTap: (index) {
|
||||
// Use the dashboard controller to handle tab navigation
|
||||
// This is typically provided by the parent Dashboard
|
||||
final dashboardController =
|
||||
Get.find<PetugasBumdesDashboardController>();
|
||||
dashboardController.changeTab(index);
|
||||
},
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.dashboard_outlined),
|
||||
activeIcon: Icon(Icons.dashboard),
|
||||
label: 'Dashboard',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.inventory_2_outlined),
|
||||
activeIcon: Icon(Icons.inventory_2),
|
||||
label: 'Aset',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.archive_outlined),
|
||||
activeIcon: Icon(Icons.archive),
|
||||
label: 'Paket',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.assignment_outlined),
|
||||
activeIcon: Icon(Icons.assignment),
|
||||
label: 'Sewa',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.subscriptions_outlined),
|
||||
activeIcon: Icon(Icons.subscriptions),
|
||||
label: 'Langganan',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.business_outlined),
|
||||
activeIcon: Icon(Icons.business),
|
||||
label: 'BUMDes',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDrawer() {
|
||||
return Drawer(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
DrawerHeader(
|
||||
decoration: BoxDecoration(color: AppColorsPetugas.navyBlue),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
backgroundColor: Colors.white,
|
||||
radius: 30,
|
||||
child: Icon(Icons.person, size: 40, color: Colors.blueGrey),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Admin BUMDes',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'admin@bumdes.desa.id',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.dashboard_outlined),
|
||||
title: const Text('Dashboard'),
|
||||
onTap: () {
|
||||
Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.inventory_2_outlined),
|
||||
title: const Text('Kelola Aset'),
|
||||
onTap: () {
|
||||
Get.offAllNamed(Routes.PETUGAS_ASET);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.feed_outlined),
|
||||
title: const Text('Kelola Paket'),
|
||||
onTap: () {
|
||||
Get.offAllNamed(Routes.PETUGAS_PAKET);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.assignment_outlined),
|
||||
title: const Text('Kelola Permintaan Sewa'),
|
||||
onTap: () {
|
||||
Get.offAllNamed(Routes.PETUGAS_SEWA);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.subscriptions_outlined),
|
||||
title: const Text('Kelola Langganan'),
|
||||
onTap: () {
|
||||
Get.offAllNamed(Routes.PETUGAS_LANGGANAN);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.business_outlined),
|
||||
title: const Text('BUMDes CBP'),
|
||||
tileColor: Colors.blue.shade50,
|
||||
onTap: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: const Text('Logout'),
|
||||
onTap: () {
|
||||
// Implement logout
|
||||
Get.offAllNamed(Routes.LOGIN);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Method to handle navigation to bank accounts management
|
||||
void _showBankAccountsPage() {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.account_balance,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text(
|
||||
'Rekening Bank',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Obx(
|
||||
() =>
|
||||
controller.bankAccounts.isEmpty
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Text(
|
||||
'Belum ada rekening yang terdaftar',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
children:
|
||||
controller.bankAccounts
|
||||
.map(
|
||||
(account) => _buildBankAccountItem(account),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
// Show the full-screen bank accounts page
|
||||
Get.snackbar(
|
||||
'Informasi',
|
||||
'Menuju halaman kelola rekening bank',
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
label: const Text('Lihat Semua Rekening'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBankAccountItem(Map<String, dynamic> account) {
|
||||
final isPrimary = account['is_primary'] as bool;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isPrimary ? AppColorsPetugas.blueGrotto : Colors.grey.shade300,
|
||||
width: isPrimary ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.credit_card,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
account['bank_name'],
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (isPrimary) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.blueGrotto.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'Utama',
|
||||
style: TextStyle(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
account['account_number'],
|
||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Method to handle navigation to partners management
|
||||
void _showPartnersPage() {
|
||||
// Navigate to the ListPetugasMitraView
|
||||
Get.toNamed(Routes.LIST_PETUGAS_MITRA);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
2054
lib/app/modules/petugas_bumdes/views/petugas_detail_sewa_view.dart
Normal file
2054
lib/app/modules/petugas_bumdes/views/petugas_detail_sewa_view.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,914 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_manajemen_bumdes_controller.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
|
||||
class PetugasManajemenBumdesView
|
||||
extends GetView<PetugasManajemenBumdesController> {
|
||||
const PetugasManajemenBumdesView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Manajemen BUMDes'),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: Obx(
|
||||
() =>
|
||||
controller.isLoading.value
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Column(
|
||||
children: [
|
||||
_buildTabBar(),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
switch (controller.selectedTabIndex.value) {
|
||||
case 0:
|
||||
return _buildProfileTab();
|
||||
case 1:
|
||||
return _buildBankAccountTab();
|
||||
case 2:
|
||||
return _buildPartnerTab();
|
||||
default:
|
||||
return _buildProfileTab();
|
||||
}
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: _buildBottomNav(dashboardController),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color(0x29000000),
|
||||
offset: Offset(0, 3),
|
||||
blurRadius: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildTab(0, 'Profile'),
|
||||
_buildTab(1, 'Rekening Bank'),
|
||||
_buildTab(2, 'Mitra'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTab(int index, String title) {
|
||||
final isSelected = controller.selectedTabIndex.value == index;
|
||||
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.changeTab(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color:
|
||||
isSelected ? AppColorsPetugas.navyBlue : Colors.transparent,
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color:
|
||||
isSelected ? AppColorsPetugas.navyBlue : Colors.grey.shade600,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileTab() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Profile BUMDes',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildProfileForm(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileForm() {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildProfileField('Nama BUMDes', 'BUMDes Sejahtera'),
|
||||
_buildProfileField('Alamat', 'Jl. Desa No. 123, Kecamatan Makmur'),
|
||||
_buildProfileField('Email', 'bumdes.sejahtera@gmail.com'),
|
||||
_buildProfileField('Telepon', '081234567890'),
|
||||
_buildProfileField(
|
||||
'Deskripsi',
|
||||
'BUMDes yang bergerak dalam bidang penyewaan aset dan paket untuk masyarakat desa.',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
minimumSize: const Size(double.infinity, 45),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Edit Profile'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileField(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: const TextStyle(fontSize: 14)),
|
||||
const Divider(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBankAccountTab() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Rekening Bank',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showAddBankAccountDialog(),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Tambah'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.bankAccounts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final account = controller.bankAccounts[index];
|
||||
return _buildBankAccountCard(account, index);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBankAccountCard(Map<String, dynamic> account, int index) {
|
||||
final isPrimary = account['isPrimary'] as bool;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(
|
||||
color: isPrimary ? AppColorsPetugas.navyBlue : Colors.transparent,
|
||||
width: isPrimary ? 2 : 0,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
account['bankName'],
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
if (!isPrimary)
|
||||
PopupMenuItem(
|
||||
value: 'primary',
|
||||
child: const Text('Jadikan Utama'),
|
||||
),
|
||||
const PopupMenuItem(value: 'edit', child: Text('Edit')),
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text('Hapus'),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'primary':
|
||||
controller.setPrimaryBankAccount(index);
|
||||
break;
|
||||
case 'edit':
|
||||
_showEditBankAccountDialog(account, index);
|
||||
break;
|
||||
case 'delete':
|
||||
_showDeleteBankAccountDialog(index);
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildBankAccountInfo('Nama Pemilik', account['accountName']),
|
||||
_buildBankAccountInfo('Nomor Rekening', account['accountNumber']),
|
||||
if (isPrimary)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.navyBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'Rekening Utama',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBankAccountInfo(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
Expanded(child: Text(value, style: const TextStyle(fontSize: 14))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPartnerTab() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Mitra BUMDes',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showAddPartnerDialog(),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Tambah'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.partners.length,
|
||||
itemBuilder: (context, index) {
|
||||
final partner = controller.partners[index];
|
||||
return _buildPartnerCard(partner, index);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPartnerCard(Map<String, dynamic> partner, int index) {
|
||||
final isActive = partner['isActive'] as bool;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
partner['name'],
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Switch(
|
||||
value: isActive,
|
||||
onChanged:
|
||||
(value) => controller.togglePartnerStatus(index),
|
||||
activeColor: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Text('Edit'),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text('Hapus'),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'edit':
|
||||
_showEditPartnerDialog(partner, index);
|
||||
break;
|
||||
case 'delete':
|
||||
_showDeletePartnerDialog(index);
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildPartnerInfo('Email', partner['email']),
|
||||
_buildPartnerInfo('Telepon', partner['phone']),
|
||||
_buildPartnerInfo('Alamat', partner['address']),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isActive
|
||||
? Colors.green.withOpacity(0.1)
|
||||
: Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
isActive ? 'Aktif' : 'Tidak Aktif',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isActive ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPartnerInfo(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
Expanded(child: Text(value, style: const TextStyle(fontSize: 14))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomNav(PetugasBumdesDashboardController dashboardController) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildNavItem(Icons.dashboard, 'Dashboard', 0, dashboardController),
|
||||
_buildNavItem(Icons.inventory, 'Aset', 1, dashboardController),
|
||||
_buildNavItem(Icons.inventory_2, 'Paket', 2, dashboardController),
|
||||
_buildNavItem(Icons.shopping_cart, 'Sewa', 3, dashboardController),
|
||||
_buildNavItem(
|
||||
Icons.subscriptions,
|
||||
'Langganan',
|
||||
4,
|
||||
dashboardController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem(
|
||||
IconData icon,
|
||||
String label,
|
||||
int index,
|
||||
PetugasBumdesDashboardController dashboardController,
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: () => dashboardController.changeTab(index),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: AppColorsPetugas.blueGrotto, size: 24),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 12, color: AppColorsPetugas.blueGrotto),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddBankAccountDialog() {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Tambah Rekening Bank',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nama Bank',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nama Pemilik Rekening',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nomor Rekening',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur tambah rekening bank sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditBankAccountDialog(Map<String, dynamic> account, int index) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Edit Rekening Bank',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nama Bank',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: account['bankName']),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nama Pemilik Rekening',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: account['accountName']),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nomor Rekening',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(
|
||||
text: account['accountNumber'],
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur edit rekening bank sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteBankAccountDialog(int index) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Hapus Rekening Bank',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Apakah Anda yakin ingin menghapus rekening bank ini?',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur hapus rekening bank sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddPartnerDialog() {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Tambah Mitra',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nama Mitra',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Telepon',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Alamat',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur tambah mitra sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditPartnerDialog(Map<String, dynamic> partner, int index) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Edit Mitra',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nama Mitra',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: partner['name']),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: partner['email']),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Telepon',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: partner['phone']),
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Alamat',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: partner['address']),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur edit mitra sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeletePartnerDialog(int index) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Hapus Mitra',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Apakah Anda yakin ingin menghapus mitra ini?',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur hapus mitra sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
700
lib/app/modules/petugas_bumdes/views/petugas_paket_view.dart
Normal file
700
lib/app/modules/petugas_bumdes/views/petugas_paket_view.dart
Normal file
@ -0,0 +1,700 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_paket_controller.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
const PetugasPaketView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get dashboard controller for navigation
|
||||
final dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
// Saat back button ditekan, kembali ke dashboard
|
||||
dashboardController.changeTab(0);
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Manajemen Paket',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sort, size: 22),
|
||||
onPressed: () => _showSortingBottomSheet(context),
|
||||
tooltip: 'Urutkan',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
drawer: PetugasSideNavbar(controller: dashboardController),
|
||||
drawerEdgeDragWidth: 60,
|
||||
drawerScrimColor: Colors.black.withOpacity(0.6),
|
||||
backgroundColor: AppColorsPetugas.babyBlueBright,
|
||||
body: Column(
|
||||
children: [_buildSearchBar(), Expanded(child: _buildPaketList())],
|
||||
),
|
||||
bottomNavigationBar: Obx(
|
||||
() => PetugasBumdesBottomNavbar(
|
||||
selectedIndex: dashboardController.currentTabIndex.value,
|
||||
onItemTapped: (index) => dashboardController.changeTab(index),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET),
|
||||
label: Text(
|
||||
'Tambah Paket',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
icon: Icon(Icons.add, color: AppColorsPetugas.blueGrotto),
|
||||
backgroundColor: AppColorsPetugas.babyBlueBright,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColorsPetugas.shadowColor,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
onChanged: controller.setSearchQuery,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari paket...',
|
||||
hintStyle: TextStyle(color: Colors.grey.shade400),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColorsPetugas.babyBlueBright,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaketList() {
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.filteredPaketList.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.category_outlined,
|
||||
size: 80,
|
||||
color: AppColorsPetugas.babyBlue,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Tidak ada paket ditemukan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Tambah Paket'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: controller.loadPaketData,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredPaketList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final paket = controller.filteredPaketList[index];
|
||||
return _buildPaketCard(context, paket);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPaketCard(BuildContext context, Map<String, dynamic> paket) {
|
||||
final isAvailable = paket['tersedia'] == true;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColorsPetugas.shadowColor,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _showPaketDetails(context, paket),
|
||||
child: Row(
|
||||
children: [
|
||||
// Paket image or icon
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
bottomLeft: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getPaketIcon(paket['kategori']),
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Paket info
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Name and price
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
paket['nama'],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Rp ${_formatPrice(paket['harga'])}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Status badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isAvailable
|
||||
? AppColorsPetugas.successLight
|
||||
: AppColorsPetugas.errorLight,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color:
|
||||
isAvailable
|
||||
? AppColorsPetugas.success
|
||||
: AppColorsPetugas.error,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
isAvailable ? 'Aktif' : 'Nonaktif',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color:
|
||||
isAvailable
|
||||
? AppColorsPetugas.success
|
||||
: AppColorsPetugas.error,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Action icons
|
||||
const SizedBox(width: 12),
|
||||
Row(
|
||||
children: [
|
||||
// Edit icon
|
||||
GestureDetector(
|
||||
onTap:
|
||||
() => _showAddEditPaketDialog(
|
||||
context,
|
||||
paket: paket,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: AppColorsPetugas.blueGrotto
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.edit_outlined,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Delete icon
|
||||
GestureDetector(
|
||||
onTap:
|
||||
() => _showDeleteConfirmation(context, paket),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.errorLight,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: AppColorsPetugas.error.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.delete_outline,
|
||||
color: AppColorsPetugas.error,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPrice(dynamic price) {
|
||||
if (price == null) return '0';
|
||||
|
||||
// Convert the price to string and handle formatting
|
||||
String priceStr = price.toString();
|
||||
|
||||
// Add thousand separators
|
||||
final RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))');
|
||||
String formatted = priceStr.replaceAllMapped(reg, (Match m) => '${m[1]}.');
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
IconData _getPaketIcon(String? category) {
|
||||
if (category == null) return Icons.category;
|
||||
|
||||
switch (category.toLowerCase()) {
|
||||
case 'bulanan':
|
||||
return Icons.calendar_month;
|
||||
case 'tahunan':
|
||||
return Icons.calendar_today;
|
||||
case 'premium':
|
||||
return Icons.star;
|
||||
case 'bisnis':
|
||||
return Icons.business;
|
||||
default:
|
||||
return Icons.category;
|
||||
}
|
||||
}
|
||||
|
||||
void _showSortingBottomSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Urutkan Paket',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...controller.sortOptions.map((option) {
|
||||
return Obx(() {
|
||||
final isSelected = option == controller.sortBy.value;
|
||||
return RadioListTile<String>(
|
||||
title: Text(option),
|
||||
value: option,
|
||||
groupValue: controller.sortBy.value,
|
||||
activeColor: AppColorsPetugas.blueGrotto,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
controller.setSortBy(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showPaketDetails(BuildContext context, Map<String, dynamic> paket) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.75,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
paket['nama'],
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, color: AppColorsPetugas.blueGrotto),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDetailItem('Kategori', paket['kategori']),
|
||||
_buildDetailItem(
|
||||
'Harga',
|
||||
controller.formatPrice(paket['harga']),
|
||||
),
|
||||
_buildDetailItem(
|
||||
'Status',
|
||||
paket['tersedia'] ? 'Tersedia' : 'Tidak Tersedia',
|
||||
),
|
||||
_buildDetailItem('Deskripsi', paket['deskripsi']),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Item dalam Paket',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListView.separated(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: paket['items'].length,
|
||||
separatorBuilder:
|
||||
(context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final item = paket['items'][index];
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColorsPetugas.babyBlue,
|
||||
child: Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
title: Text(item['nama']),
|
||||
trailing: Text(
|
||||
'${item['jumlah']} unit',
|
||||
style: TextStyle(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_PAKET,
|
||||
arguments: paket,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.edit),
|
||||
label: const Text('Edit'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColorsPetugas.blueGrotto,
|
||||
side: BorderSide(color: AppColorsPetugas.blueGrotto),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_showDeleteConfirmation(context, paket);
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
label: const Text('Hapus'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.error,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailItem(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 14, color: AppColorsPetugas.blueGrotto),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddEditPaketDialog(
|
||||
BuildContext context, {
|
||||
Map<String, dynamic>? paket,
|
||||
}) {
|
||||
final isEditing = paket != null;
|
||||
|
||||
// This would be implemented with proper form validation in a real app
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
isEditing ? 'Edit Paket' : 'Tambah Paket Baru',
|
||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
content: const Text(
|
||||
'Form pengelolaan paket akan ditampilkan di sini dengan field untuk nama, kategori, harga, deskripsi, status, dan item-item dalam paket.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Batal',
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
// In a real app, we would save the form data
|
||||
Get.snackbar(
|
||||
isEditing ? 'Paket Diperbarui' : 'Paket Ditambahkan',
|
||||
isEditing
|
||||
? 'Paket berhasil diperbarui'
|
||||
: 'Paket baru berhasil ditambahkan',
|
||||
backgroundColor: AppColorsPetugas.success,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
child: Text(isEditing ? 'Simpan' : 'Tambah'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmation(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> paket,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'Konfirmasi Hapus',
|
||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
content: Text(
|
||||
'Apakah Anda yakin ingin menghapus paket "${paket['nama']}"?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Batal',
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
controller.deletePaket(paket['id']);
|
||||
Get.snackbar(
|
||||
'Paket Dihapus',
|
||||
'Paket berhasil dihapus dari sistem',
|
||||
backgroundColor: AppColorsPetugas.error,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.error,
|
||||
),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
682
lib/app/modules/petugas_bumdes/views/petugas_sewa_view.dart
Normal file
682
lib/app/modules/petugas_bumdes/views/petugas_sewa_view.dart
Normal file
@ -0,0 +1,682 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_sewa_controller.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import 'petugas_detail_sewa_view.dart';
|
||||
|
||||
class PetugasSewaView extends StatefulWidget {
|
||||
const PetugasSewaView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PetugasSewaView> createState() => _PetugasSewaViewState();
|
||||
}
|
||||
|
||||
class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
late PetugasSewaController controller;
|
||||
late PetugasBumdesDashboardController dashboardController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = Get.find<PetugasSewaController>();
|
||||
dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||
|
||||
_tabController = TabController(
|
||||
length: controller.statusFilters.length,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// Add listener to sync tab selection with controller's filter
|
||||
_tabController.addListener(_onTabChanged);
|
||||
|
||||
// Listen to controller's filter changes
|
||||
ever(controller.selectedStatusFilter, _onFilterChanged);
|
||||
}
|
||||
|
||||
void _onTabChanged() {
|
||||
if (!_tabController.indexIsChanging) {
|
||||
final selectedStatus = controller.statusFilters[_tabController.index];
|
||||
controller.setStatusFilter(selectedStatus);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFilterChanged(String status) {
|
||||
final index = controller.statusFilters.indexOf(status);
|
||||
if (index != -1 && index != _tabController.index) {
|
||||
_tabController.animateTo(index);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.removeListener(_onTabChanged);
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
dashboardController.changeTab(0);
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Manajemen Sewa',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.filter_alt_outlined, size: 22),
|
||||
onPressed: () => _showFilterBottomSheet(),
|
||||
tooltip: 'Filter',
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(60),
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
indicator: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicatorPadding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 4,
|
||||
),
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.white.withOpacity(0.7),
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
tabs:
|
||||
controller.statusFilters
|
||||
.map(
|
||||
(status) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Tab(text: status),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
dividerColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
drawer: PetugasSideNavbar(controller: dashboardController),
|
||||
drawerEdgeDragWidth: 60,
|
||||
drawerScrimColor: Colors.black.withOpacity(0.6),
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildSearchSection(),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children:
|
||||
controller.statusFilters.map((status) {
|
||||
return _buildSewaListForStatus(status);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: Obx(
|
||||
() => PetugasBumdesBottomNavbar(
|
||||
selectedIndex: dashboardController.currentTabIndex.value,
|
||||
onItemTapped: (index) => dashboardController.changeTab(index),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
onChanged: (value) {
|
||||
controller.setSearchQuery(value);
|
||||
controller.setOrderIdQuery(value);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari nama warga atau ID pesanan...',
|
||||
hintStyle: TextStyle(color: Colors.grey.shade400, fontSize: 14),
|
||||
prefixIcon: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Icon(
|
||||
Icons.search_rounded,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true,
|
||||
suffixIcon: Icon(
|
||||
Icons.tune_rounded,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSewaListForStatus(String status) {
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Memuat data...',
|
||||
style: TextStyle(
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final filteredList =
|
||||
status == 'Semua'
|
||||
? controller.filteredSewaList
|
||||
: status == 'Periksa Pembayaran'
|
||||
? controller.sewaList
|
||||
.where(
|
||||
(sewa) =>
|
||||
sewa['status'] == 'Periksa Pembayaran' ||
|
||||
sewa['status'] == 'Pembayaran Denda' ||
|
||||
sewa['status'] == 'Periksa Denda',
|
||||
)
|
||||
.toList()
|
||||
: controller.sewaList
|
||||
.where((sewa) => sewa['status'] == status)
|
||||
.toList();
|
||||
|
||||
if (filteredList.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 70,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Tidak ada sewa ditemukan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
status == 'Semua'
|
||||
? 'Belum ada data sewa untuk kriteria yang dipilih'
|
||||
: status == 'Periksa Pembayaran'
|
||||
? 'Belum ada data sewa yang perlu pembayaran diverifikasi atau memiliki denda'
|
||||
: 'Belum ada data sewa dengan status "$status"',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: controller.loadSewaData,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: filteredList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final sewa = filteredList[index];
|
||||
return _buildSewaCard(context, sewa);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildSewaCard(BuildContext context, Map<String, dynamic> sewa) {
|
||||
final statusColor = controller.getStatusColor(sewa['status']);
|
||||
final status = sewa['status'];
|
||||
|
||||
// Get appropriate icon for status
|
||||
IconData statusIcon;
|
||||
switch (status) {
|
||||
case 'Menunggu Pembayaran':
|
||||
statusIcon = Icons.payments_outlined;
|
||||
break;
|
||||
case 'Periksa Pembayaran':
|
||||
statusIcon = Icons.fact_check_outlined;
|
||||
break;
|
||||
case 'Diterima':
|
||||
statusIcon = Icons.check_circle_outlined;
|
||||
break;
|
||||
case 'Pembayaran Denda':
|
||||
statusIcon = Icons.money_off_csred_outlined;
|
||||
break;
|
||||
case 'Periksa Denda':
|
||||
statusIcon = Icons.assignment_late_outlined;
|
||||
break;
|
||||
case 'Dikembalikan':
|
||||
statusIcon = Icons.assignment_return_outlined;
|
||||
break;
|
||||
case 'Selesai':
|
||||
statusIcon = Icons.task_alt_outlined;
|
||||
break;
|
||||
case 'Dibatalkan':
|
||||
statusIcon = Icons.cancel_outlined;
|
||||
break;
|
||||
default:
|
||||
statusIcon = Icons.help_outline_rounded;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: InkWell(
|
||||
onTap: () => Get.to(() => PetugasDetailSewaView(sewa: sewa)),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Customer Circle Avatar
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||
child: Text(
|
||||
sewa['nama_warga'].substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Customer details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sewa['nama_warga'],
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
statusIcon,
|
||||
size: 12,
|
||||
color: statusColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'#${sewa['order_id']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Price
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.blueGrotto.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
controller.formatPrice(sewa['total_biaya']),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Divider
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Divider(height: 1, color: Colors.grey.shade200),
|
||||
),
|
||||
|
||||
// Asset details
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
// Asset icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 20,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Asset name and duration
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sewa['nama_aset'],
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today_rounded,
|
||||
size: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${sewa['tanggal_mulai']} - ${sewa['tanggal_selesai']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Chevron icon
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFilterBottomSheet() {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Filter',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Status',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Obx(() {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
children:
|
||||
controller.statusFilters.map((status) {
|
||||
final isSelected =
|
||||
status == controller.selectedStatusFilter.value;
|
||||
return ChoiceChip(
|
||||
label: Text(status),
|
||||
selected: isSelected,
|
||||
selectedColor: AppColorsPetugas.blueGrotto,
|
||||
backgroundColor: Colors.white,
|
||||
labelStyle: TextStyle(
|
||||
color:
|
||||
isSelected
|
||||
? Colors.white
|
||||
: AppColorsPetugas.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: BorderSide(
|
||||
color:
|
||||
isSelected
|
||||
? AppColorsPetugas.blueGrotto
|
||||
: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
controller.setStatusFilter(status);
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
controller.resetFilters();
|
||||
Get.back();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: AppColorsPetugas.blueGrotto),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Reset',
|
||||
style: TextStyle(color: AppColorsPetugas.blueGrotto),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
controller.applyFilters();
|
||||
Get.back();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Terapkan',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
isDismissible: true,
|
||||
enableDrag: true,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,871 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../controllers/petugas_tambah_aset_controller.dart';
|
||||
|
||||
class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
const PetugasTambahAsetView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Tambah Aset',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [_buildHeaderSection(), _buildFormSection(context)],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: _buildBottomBar(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Informasi Aset Baru',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Isi data dengan lengkap untuk menambahkan aset',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormSection(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Basic Information Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.info_outline,
|
||||
title: 'Informasi Dasar',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
label: 'Nama Aset',
|
||||
hint: 'Masukkan nama aset',
|
||||
controller: controller.nameController,
|
||||
isRequired: true,
|
||||
prefixIcon: Icons.title,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
label: 'Deskripsi',
|
||||
hint: 'Masukkan deskripsi aset',
|
||||
controller: controller.descriptionController,
|
||||
maxLines: 3,
|
||||
isRequired: true,
|
||||
prefixIcon: Icons.description,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Media Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.photo_library,
|
||||
title: 'Media & Gambar',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildImageUploader(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Category Section
|
||||
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Category and Status as cards
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Kategori',
|
||||
options: controller.categoryOptions,
|
||||
selectedOption: controller.selectedCategory,
|
||||
onChanged: controller.setCategory,
|
||||
icon: Icons.inventory_2,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Status',
|
||||
options: controller.statusOptions,
|
||||
selectedOption: controller.selectedStatus,
|
||||
onChanged: controller.setStatus,
|
||||
icon: Icons.check_circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Quantity Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.format_list_numbered,
|
||||
title: 'Kuantitas & Pengukuran',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quantity fields
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildTextField(
|
||||
label: 'Kuantitas',
|
||||
hint: 'Jumlah aset',
|
||||
controller: controller.quantityController,
|
||||
isRequired: true,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
prefixIcon: Icons.numbers,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: _buildTextField(
|
||||
label: 'Satuan Ukur',
|
||||
hint: 'contoh: Unit, Buah',
|
||||
controller: controller.unitOfMeasureController,
|
||||
prefixIcon: Icons.straighten,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Rental Options Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.schedule,
|
||||
title: 'Opsi Waktu & Harga Sewa',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Time Options as cards
|
||||
_buildTimeOptionsCards(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Rental price fields based on selection
|
||||
Obx(
|
||||
() => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Per Hour Option
|
||||
if (controller.timeOptions['Per Jam']!.value)
|
||||
_buildPriceCard(
|
||||
title: 'Harga Per Jam',
|
||||
icon: Icons.timer,
|
||||
priceController: controller.pricePerHourController,
|
||||
maxController: controller.maxHourController,
|
||||
maxLabel: 'Maksimal Jam',
|
||||
),
|
||||
|
||||
if (controller.timeOptions['Per Jam']!.value &&
|
||||
controller.timeOptions['Per Hari']!.value)
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Per Day Option
|
||||
if (controller.timeOptions['Per Hari']!.value)
|
||||
_buildPriceCard(
|
||||
title: 'Harga Per Hari',
|
||||
icon: Icons.calendar_today,
|
||||
priceController: controller.pricePerDayController,
|
||||
maxController: controller.maxDayController,
|
||||
maxLabel: 'Maksimal Hari',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeOptionsCards() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children:
|
||||
controller.timeOptions.entries.map((entry) {
|
||||
final option = entry.key;
|
||||
final isSelected = entry.value;
|
||||
|
||||
return Obx(
|
||||
() => Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => controller.toggleTimeOption(option),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isSelected.value
|
||||
? AppColorsPetugas.blueGrotto.withOpacity(
|
||||
0.1,
|
||||
)
|
||||
: Colors.grey.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
option == 'Per Jam'
|
||||
? Icons.hourglass_bottom
|
||||
: Icons.calendar_today,
|
||||
color:
|
||||
isSelected.value
|
||||
? AppColorsPetugas.blueGrotto
|
||||
: Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
option,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color:
|
||||
isSelected.value
|
||||
? AppColorsPetugas.navyBlue
|
||||
: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
option == 'Per Jam'
|
||||
? 'Sewa aset dengan basis perhitungan per jam'
|
||||
: 'Sewa aset dengan basis perhitungan per hari',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Checkbox(
|
||||
value: isSelected.value,
|
||||
onChanged:
|
||||
(_) => controller.toggleTimeOption(option),
|
||||
activeColor: AppColorsPetugas.blueGrotto,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceCard({
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required TextEditingController priceController,
|
||||
required TextEditingController maxController,
|
||||
required String maxLabel,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColorsPetugas.babyBlue.withOpacity(0.5)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: AppColorsPetugas.blueGrotto),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Harga Sewa',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: priceController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Masukkan harga',
|
||||
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
|
||||
prefixText: 'Rp ',
|
||||
filled: true,
|
||||
fillColor: AppColorsPetugas.babyBlueBright,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
maxLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: maxController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Opsional',
|
||||
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
|
||||
filled: true,
|
||||
fillColor: AppColorsPetugas.babyBlueBright,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader({required IconData icon, required String title}) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.blueGrotto.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: AppColorsPetugas.blueGrotto, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField({
|
||||
required String label,
|
||||
required String hint,
|
||||
required TextEditingController controller,
|
||||
bool isRequired = false,
|
||||
int maxLines = 1,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
String? prefixText,
|
||||
IconData? prefixIcon,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
if (isRequired) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'*',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
maxLines: maxLines,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
|
||||
filled: true,
|
||||
fillColor: AppColorsPetugas.babyBlueBright,
|
||||
prefixText: prefixText,
|
||||
prefixIcon:
|
||||
prefixIcon != null
|
||||
? Icon(
|
||||
prefixIcon,
|
||||
size: 20,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategorySelect({
|
||||
required String title,
|
||||
required List<String> options,
|
||||
required RxString selectedOption,
|
||||
required Function(String) onChanged,
|
||||
required IconData icon,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Obx(
|
||||
() => DropdownButtonFormField<String>(
|
||||
value: selectedOption.value,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(
|
||||
icon,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 20,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColorsPetugas.babyBlueBright,
|
||||
),
|
||||
items:
|
||||
options.map((option) {
|
||||
return DropdownMenuItem(
|
||||
value: option,
|
||||
child: Text(
|
||||
option,
|
||||
style: TextStyle(
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) onChanged(value);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
dropdownColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageUploader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColorsPetugas.babyBlue.withOpacity(0.5)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Unggah Foto Aset',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Tambahkan foto aset untuk informasi visual.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Obx(
|
||||
() => Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
// Add button
|
||||
GestureDetector(
|
||||
onTap: () => controller.addSampleImage(),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: AppColorsPetugas.babyBlue,
|
||||
width: 1,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_photo_alternate_outlined,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tambah Foto',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Image previews
|
||||
...controller.selectedImages.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.removeImage(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: AppColorsPetugas.error,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
label: const Text('Batal'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColorsPetugas.textSecondary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
side: BorderSide(color: AppColorsPetugas.divider),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
final isValid = controller.isFormValid.value;
|
||||
final isSubmitting = controller.isSubmitting.value;
|
||||
return ElevatedButton.icon(
|
||||
onPressed:
|
||||
isValid && !isSubmitting ? controller.saveAsset : null,
|
||||
icon:
|
||||
isSubmitting
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.save),
|
||||
label: Text(isSubmitting ? 'Menyimpan...' : 'Simpan Aset'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
disabledBackgroundColor: AppColorsPetugas.textLight,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,932 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../controllers/petugas_tambah_paket_controller.dart';
|
||||
|
||||
class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
const PetugasTambahPaketView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Tambah Paket',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [_buildHeaderSection(), _buildFormSection(context)],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: _buildBottomBar(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.category,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Informasi Paket Baru',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Isi data dengan lengkap untuk menambahkan paket',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormSection(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Basic Information Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.info_outline,
|
||||
title: 'Informasi Dasar',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
label: 'Nama Paket',
|
||||
hint: 'Masukkan nama paket',
|
||||
controller: controller.nameController,
|
||||
isRequired: true,
|
||||
prefixIcon: Icons.title,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
label: 'Deskripsi',
|
||||
hint: 'Masukkan deskripsi paket',
|
||||
controller: controller.descriptionController,
|
||||
maxLines: 3,
|
||||
isRequired: true,
|
||||
prefixIcon: Icons.description,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Media Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.photo_library,
|
||||
title: 'Media & Gambar',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildImageUploader(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Category Section
|
||||
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Category and Status as cards
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Kategori',
|
||||
options: controller.categoryOptions,
|
||||
selectedOption: controller.selectedCategory,
|
||||
onChanged: controller.setCategory,
|
||||
icon: Icons.category,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Status',
|
||||
options: controller.statusOptions,
|
||||
selectedOption: controller.selectedStatus,
|
||||
onChanged: controller.setStatus,
|
||||
icon: Icons.check_circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Price Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.monetization_on,
|
||||
title: 'Harga Paket',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
label: 'Harga Paket',
|
||||
hint: 'Masukkan harga paket',
|
||||
controller: controller.priceController,
|
||||
isRequired: true,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
prefixText: 'Rp ',
|
||||
prefixIcon: Icons.payments,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Package Items Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.inventory_2,
|
||||
title: 'Item dalam Paket',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildPackageItems(),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPackageItems() {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Item Paket',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showAddItemDialog(),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Tambah Item'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||
foregroundColor: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Obx(
|
||||
() =>
|
||||
controller.packageItems.isEmpty
|
||||
? const Center(
|
||||
child: Text(
|
||||
'Belum ada item dalam paket',
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: controller.packageItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = controller.packageItems[index];
|
||||
return Card(
|
||||
elevation: 1,
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
title: Text(item['nama'] ?? 'Item Paket'),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Jumlah: ${item['jumlah']}'),
|
||||
if (item['stok'] != null)
|
||||
Text('Stok tersedia: ${item['stok']}'),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.edit,
|
||||
color: Colors.blue,
|
||||
),
|
||||
onPressed: () => _showEditItemDialog(index),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.delete,
|
||||
color: Colors.red,
|
||||
),
|
||||
onPressed:
|
||||
() => controller.removeItem(index),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddItemDialog() {
|
||||
// Reset controllers
|
||||
controller.selectedAsset.value = null;
|
||||
controller.itemQuantityController.clear();
|
||||
|
||||
// Fetch available assets
|
||||
controller.fetchAvailableAssets();
|
||||
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
if (controller.isLoadingAssets.value) {
|
||||
return const SizedBox(
|
||||
height: 150,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'Tambah Item ke Paket',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Asset dropdown
|
||||
DropdownButtonFormField<int>(
|
||||
value: controller.selectedAsset.value,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pilih Aset',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
hint: const Text('Pilih Aset'),
|
||||
items:
|
||||
controller.availableAssets.map((asset) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: asset['id'] as int,
|
||||
child: Text(
|
||||
'${asset['nama']} (Stok: ${asset['stok']})',
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
controller.setSelectedAsset(value);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quantity field
|
||||
Obx(() {
|
||||
// Calculate max quantity based on selected asset
|
||||
String? helperText;
|
||||
if (controller.selectedAsset.value != null) {
|
||||
final remaining = controller.getRemainingStock(
|
||||
controller.selectedAsset.value!,
|
||||
);
|
||||
helperText = 'Maksimal: $remaining unit';
|
||||
}
|
||||
|
||||
return TextFormField(
|
||||
controller: controller.itemQuantityController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Jumlah',
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: helperText,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
);
|
||||
}),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
controller.addAssetToPackage();
|
||||
Get.back();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||
foregroundColor: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
child: const Text('Tambah'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditItemDialog(int index) {
|
||||
final item = controller.packageItems[index];
|
||||
|
||||
// Set controllers
|
||||
controller.selectedAsset.value = item['asetId'];
|
||||
controller.itemQuantityController.text = item['jumlah'].toString();
|
||||
|
||||
// Fetch available assets
|
||||
controller.fetchAvailableAssets();
|
||||
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
if (controller.isLoadingAssets.value) {
|
||||
return const SizedBox(
|
||||
height: 150,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'Edit Item Paket',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Asset dropdown
|
||||
DropdownButtonFormField<int>(
|
||||
value: controller.selectedAsset.value,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pilih Aset',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
hint: const Text('Pilih Aset'),
|
||||
items:
|
||||
controller.availableAssets.map((asset) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: asset['id'] as int,
|
||||
child: Text(
|
||||
'${asset['nama']} (Stok: ${asset['stok']})',
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
controller.setSelectedAsset(value);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quantity field
|
||||
Obx(() {
|
||||
// Calculate max quantity based on selected asset
|
||||
String? helperText;
|
||||
if (controller.selectedAsset.value != null) {
|
||||
// Get the appropriate max quantity for editing
|
||||
final currentItem = controller.packageItems[index];
|
||||
final isCurrentAsset =
|
||||
currentItem['asetId'] == controller.selectedAsset.value;
|
||||
|
||||
int maxQuantity;
|
||||
if (isCurrentAsset) {
|
||||
// For same asset, include current quantity in calculation
|
||||
final asset = controller.availableAssets.firstWhere(
|
||||
(a) => a['id'] == controller.selectedAsset.value,
|
||||
orElse: () => {'stok': 0},
|
||||
);
|
||||
|
||||
final totalUsed = controller.packageItems
|
||||
.where(
|
||||
(item) =>
|
||||
item['asetId'] ==
|
||||
controller.selectedAsset.value &&
|
||||
controller.packageItems.indexOf(item) != index,
|
||||
)
|
||||
.fold(
|
||||
0,
|
||||
(sum, item) => sum + (item['jumlah'] as int),
|
||||
);
|
||||
|
||||
maxQuantity = (asset['stok'] as int) - totalUsed;
|
||||
} else {
|
||||
// For different asset, use remaining stock
|
||||
maxQuantity = controller.getRemainingStock(
|
||||
controller.selectedAsset.value!,
|
||||
);
|
||||
}
|
||||
|
||||
helperText = 'Maksimal: $maxQuantity unit';
|
||||
}
|
||||
|
||||
return TextFormField(
|
||||
controller: controller.itemQuantityController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Jumlah',
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: helperText,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
);
|
||||
}),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
controller.updatePackageItem(index);
|
||||
Get.back();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||
foregroundColor: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader({required IconData icon, required String title}) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.blueGrotto.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: AppColorsPetugas.blueGrotto, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField({
|
||||
required String label,
|
||||
required String hint,
|
||||
required TextEditingController controller,
|
||||
bool isRequired = false,
|
||||
int maxLines = 1,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
String? prefixText,
|
||||
IconData? prefixIcon,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
if (isRequired) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'*',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
maxLines: maxLines,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
|
||||
filled: true,
|
||||
fillColor: AppColorsPetugas.babyBlueBright,
|
||||
prefixText: prefixText,
|
||||
prefixIcon:
|
||||
prefixIcon != null
|
||||
? Icon(
|
||||
prefixIcon,
|
||||
size: 20,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategorySelect({
|
||||
required String title,
|
||||
required List<String> options,
|
||||
required RxString selectedOption,
|
||||
required Function(String) onChanged,
|
||||
required IconData icon,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Obx(
|
||||
() => DropdownButtonFormField<String>(
|
||||
value: selectedOption.value,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(
|
||||
icon,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 20,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColorsPetugas.babyBlueBright,
|
||||
),
|
||||
items:
|
||||
options.map((option) {
|
||||
return DropdownMenuItem(
|
||||
value: option,
|
||||
child: Text(
|
||||
option,
|
||||
style: TextStyle(
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) onChanged(value);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
dropdownColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageUploader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColorsPetugas.babyBlue.withOpacity(0.5)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Unggah Foto Paket',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Tambahkan foto paket untuk informasi visual.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Obx(
|
||||
() => Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
// Add button
|
||||
GestureDetector(
|
||||
onTap: () => controller.addSampleImage(),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: AppColorsPetugas.babyBlue,
|
||||
width: 1,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_photo_alternate_outlined,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tambah Foto',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Image previews
|
||||
...controller.selectedImages.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.removeImage(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: AppColorsPetugas.error,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
label: const Text('Batal'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColorsPetugas.textSecondary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
side: BorderSide(color: AppColorsPetugas.divider),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
final isValid = controller.isFormValid.value;
|
||||
final isSubmitting = controller.isSubmitting.value;
|
||||
return ElevatedButton.icon(
|
||||
onPressed:
|
||||
isValid && !isSubmitting ? controller.savePaket : null,
|
||||
icon:
|
||||
isSubmitting
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.save),
|
||||
label: Text(isSubmitting ? 'Menyimpan...' : 'Simpan Paket'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
disabledBackgroundColor: AppColorsPetugas.textLight,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
|
||||
class PetugasBumdesBottomNavbar extends StatelessWidget {
|
||||
final int selectedIndex;
|
||||
final Function(int) onItemTapped;
|
||||
|
||||
const PetugasBumdesBottomNavbar({
|
||||
super.key,
|
||||
required this.selectedIndex,
|
||||
required this.onItemTapped,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 76,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.07),
|
||||
blurRadius: 14,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
icon: Icons.dashboard_outlined,
|
||||
activeIcon: Icons.dashboard,
|
||||
label: 'Dashboard',
|
||||
isSelected: selectedIndex == 0,
|
||||
onTap: () => onItemTapped(0),
|
||||
),
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
icon: Icons.inventory_2_outlined,
|
||||
activeIcon: Icons.inventory_2,
|
||||
label: 'Aset',
|
||||
isSelected: selectedIndex == 1,
|
||||
onTap: () => onItemTapped(1),
|
||||
),
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
icon: Icons.category_outlined,
|
||||
activeIcon: Icons.category,
|
||||
label: 'Paket',
|
||||
isSelected: selectedIndex == 2,
|
||||
onTap: () => onItemTapped(2),
|
||||
),
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
icon: Icons.shopping_cart_outlined,
|
||||
activeIcon: Icons.shopping_cart,
|
||||
label: 'Sewa',
|
||||
isSelected: selectedIndex == 3,
|
||||
onTap: () => onItemTapped(3),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Modern navigation item for bottom bar
|
||||
Widget _buildNavItem({
|
||||
required BuildContext context,
|
||||
required IconData icon,
|
||||
required IconData activeIcon,
|
||||
required String label,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final primaryColor = AppColors.primary;
|
||||
final tabWidth = MediaQuery.of(context).size.width / 4;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
customBorder: const StadiumBorder(),
|
||||
splashColor: primaryColor.withOpacity(0.1),
|
||||
highlightColor: primaryColor.withOpacity(0.05),
|
||||
child: SizedBox(
|
||||
width: tabWidth,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Indicator line at top
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
height: 2,
|
||||
width: tabWidth * 0.5,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? primaryColor : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
),
|
||||
),
|
||||
|
||||
// Icon with animated scale effect when selected
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: EdgeInsets.all(isSelected ? 8 : 0),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isSelected
|
||||
? primaryColor.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
isSelected ? activeIcon : icon,
|
||||
color: isSelected ? primaryColor : Colors.grey.shade400,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Label with animated opacity
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: isSelected ? primaryColor : Colors.grey.shade500,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
302
lib/app/modules/petugas_bumdes/widgets/petugas_side_navbar.dart
Normal file
302
lib/app/modules/petugas_bumdes/widgets/petugas_side_navbar.dart
Normal file
@ -0,0 +1,302 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
|
||||
class PetugasSideNavbar extends StatelessWidget {
|
||||
final PetugasBumdesDashboardController controller;
|
||||
|
||||
const PetugasSideNavbar({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(0),
|
||||
bottomRight: Radius.circular(0),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
Expanded(child: _buildMenu()),
|
||||
_buildFooter(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
|
||||
color: AppColors.primary,
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.white,
|
||||
child: Icon(Icons.person, color: AppColors.primary, size: 36),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Petugas BUMDes',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Obx(
|
||||
() => Text(
|
||||
controller.userEmail.value,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenu() {
|
||||
return Obx(
|
||||
() => ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
children: [
|
||||
_buildSectionHeader('Menu Utama'),
|
||||
_buildMenuItem(
|
||||
icon: Icons.dashboard_outlined,
|
||||
activeIcon: Icons.dashboard,
|
||||
title: 'Dashboard',
|
||||
subtitle: 'Ringkasan aktivitas',
|
||||
isSelected: controller.currentTabIndex.value == 0,
|
||||
onTap: () => controller.changeTab(0),
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.inventory_2_outlined,
|
||||
activeIcon: Icons.inventory_2,
|
||||
title: 'Aset',
|
||||
subtitle: 'Kelola aset BUMDes',
|
||||
isSelected: controller.currentTabIndex.value == 1,
|
||||
onTap: () => controller.changeTab(1),
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.category_outlined,
|
||||
activeIcon: Icons.category,
|
||||
title: 'Paket',
|
||||
subtitle: 'Kelola paket aset',
|
||||
isSelected: controller.currentTabIndex.value == 2,
|
||||
onTap: () => controller.changeTab(2),
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.shopping_cart_outlined,
|
||||
activeIcon: Icons.shopping_cart,
|
||||
title: 'Sewa',
|
||||
subtitle: 'Kelola sewa aset',
|
||||
isSelected: controller.currentTabIndex.value == 3,
|
||||
onTap: () => controller.changeTab(3),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade500,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenuItem({
|
||||
required IconData icon,
|
||||
required IconData activeIcon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.primarySoft : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isSelected
|
||||
? AppColors.primary.withOpacity(0.15)
|
||||
: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
isSelected ? activeIcon : icon,
|
||||
color: isSelected ? AppColors.primary : Colors.grey.shade600,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isSelected ? AppColors.primary : Colors.black87,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.w500,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
|
||||
),
|
||||
trailing:
|
||||
isSelected
|
||||
? Container(
|
||||
width: 4,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onTap: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.shade200,
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, -1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(Icons.logout, color: Colors.red.shade400, size: 20),
|
||||
),
|
||||
title: const Text(
|
||||
'Keluar',
|
||||
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 15),
|
||||
),
|
||||
subtitle: const Text(
|
||||
'Keluar dari aplikasi',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
onTap: () => _showLogoutConfirmation(context),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'© 2025 BumRent App',
|
||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Image.asset(
|
||||
'assets/images/logo.png',
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showLogoutConfirmation(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Konfirmasi Keluar'),
|
||||
content: const Text('Apakah Anda yakin ingin keluar dari aplikasi?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.grey.shade700,
|
||||
),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
controller.logout();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade400,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Keluar'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user