Compare commits
5 Commits
andreasmal
...
fitur-tert
| Author | SHA1 | Date | |
|---|---|---|---|
| 47766bbdda | |||
| 0423c2fdf9 | |||
| 8284c93aa5 | |||
| c4dd4fdfa2 | |||
| 046eac48e8 |
@ -1,8 +1,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="bumrent_app"
|
||||
android:label="BumRent"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/launcher_icon">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
assets/images/logo_app.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
@ -427,7 +427,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@ -484,7 +484,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 610 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 988 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 11 KiB |
@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Bumrent App</string>
|
||||
<string>BumRent</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>bumrent_app</string>
|
||||
<string>BumRent</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
@ -19,12 +19,12 @@ class PetugasBumdesBinding extends Bindings {
|
||||
print('Error removing controller: $e');
|
||||
}
|
||||
|
||||
// Gunakan put untuk memastikan controller selalu tersedia dan permanent
|
||||
Get.put<PetugasBumdesDashboardController>(
|
||||
PetugasBumdesDashboardController(),
|
||||
permanent: true,
|
||||
// Gunakan lazyPut untuk memastikan controller hanya diinisialisasi saat dibutuhkan
|
||||
Get.lazyPut<PetugasBumdesDashboardController>(
|
||||
() => PetugasBumdesDashboardController(),
|
||||
fenix: true, // Akan dibuat ulang jika dihapus
|
||||
);
|
||||
|
||||
print('✅ PetugasBumdesDashboardController registered successfully');
|
||||
print('✅ PetugasBumdesDashboardController initialized successfully');
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,15 @@ import '../modules/warga/controllers/warga_dashboard_controller.dart';
|
||||
class WargaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<AuthProvider>(() => AuthProvider());
|
||||
Get.lazyPut<WargaDashboardController>(() => WargaDashboardController());
|
||||
// Pastikan AuthProvider teregistrasi
|
||||
if (!Get.isRegistered<AuthProvider>()) {
|
||||
Get.put(AuthProvider());
|
||||
}
|
||||
|
||||
// Gunakan lazyPut untuk memastikan controller hanya diinisialisasi saat dibutuhkan
|
||||
Get.lazyPut<WargaDashboardController>(
|
||||
() => WargaDashboardController(),
|
||||
fenix: true, // Akan dibuat ulang jika dihapus
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ class AsetModel {
|
||||
final String nama;
|
||||
final String deskripsi;
|
||||
final String kategori;
|
||||
final String jenis; // Add this line
|
||||
final int harga;
|
||||
final int? denda;
|
||||
final String status;
|
||||
@ -14,17 +15,21 @@ class AsetModel {
|
||||
final int? kuantitasTerpakai;
|
||||
final String? satuanUkur;
|
||||
|
||||
// Untuk menampung URL gambar pertama dari tabel foto_aset
|
||||
// URL gambar utama (untuk backward compatibility)
|
||||
String? imageUrl;
|
||||
|
||||
// List untuk menyimpan semua URL gambar aset
|
||||
final RxList<String> imageUrls = <String>[].obs;
|
||||
|
||||
// Menggunakan RxList untuk membuatnya mutable dan reaktif
|
||||
RxList<Map<String, dynamic>> satuanWaktuSewa = <Map<String, dynamic>>[].obs;
|
||||
final RxList<Map<String, dynamic>> satuanWaktuSewa = <Map<String, dynamic>>[].obs;
|
||||
|
||||
AsetModel({
|
||||
required this.id,
|
||||
required this.nama,
|
||||
required this.deskripsi,
|
||||
required this.kategori,
|
||||
this.jenis = 'Sewa', // Add this line with default value
|
||||
required this.harga,
|
||||
this.denda,
|
||||
required this.status,
|
||||
@ -42,31 +47,69 @@ class AsetModel {
|
||||
}
|
||||
}
|
||||
|
||||
// Menambahkan URL gambar dari JSON
|
||||
void addImageUrl(String? url) {
|
||||
if (url != null && url.isNotEmpty && !imageUrls.contains(url)) {
|
||||
imageUrls.add(url);
|
||||
// Update imageUrl untuk backward compatibility
|
||||
if (imageUrl == null) {
|
||||
imageUrl = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Menghapus URL gambar
|
||||
bool removeImageUrl(String url) {
|
||||
final removed = imageUrls.remove(url);
|
||||
if (removed && imageUrl == url) {
|
||||
imageUrl = imageUrls.isNotEmpty ? imageUrls.first : null;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
factory AsetModel.fromJson(Map<String, dynamic> json) {
|
||||
return AsetModel(
|
||||
final model = AsetModel(
|
||||
id: json['id'] ?? '',
|
||||
nama: json['nama'] ?? '',
|
||||
deskripsi: json['deskripsi'] ?? '',
|
||||
kategori: json['kategori'] ?? '',
|
||||
jenis: json['jenis'] ?? 'Sewa',
|
||||
harga: json['harga'] ?? 0,
|
||||
denda: json['denda'],
|
||||
status: json['status'] ?? '',
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
kuantitas: json['kuantitas'],
|
||||
kuantitasTerpakai: json['kuantitas_terpakai'],
|
||||
satuanUkur: json['satuan_ukur'],
|
||||
imageUrl: json['foto_aset'],
|
||||
initialSatuanWaktuSewa: json['satuan_waktu_sewa'] != null
|
||||
? List<Map<String, dynamic>>.from(json['satuan_waktu_sewa'])
|
||||
: null,
|
||||
);
|
||||
|
||||
// Add the main image URL to the list if it exists
|
||||
if (json['foto_aset'] != null) {
|
||||
model.addImageUrl(json['foto_aset']);
|
||||
}
|
||||
|
||||
// Add any additional image URLs if they exist in the JSON
|
||||
if (json['foto_aset_tambahan'] != null) {
|
||||
final additionalImages = List<String>.from(json['foto_aset_tambahan']);
|
||||
for (final url in additionalImages) {
|
||||
model.addImageUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
final data = <String, dynamic>{
|
||||
'id': id,
|
||||
'nama': nama,
|
||||
'deskripsi': deskripsi,
|
||||
@ -80,5 +123,23 @@ class AsetModel {
|
||||
'kuantitas_terpakai': kuantitasTerpakai,
|
||||
'satuan_ukur': satuanUkur,
|
||||
};
|
||||
|
||||
// Add image URLs if they exist
|
||||
if (imageUrls.isNotEmpty) {
|
||||
data['foto_aset'] = imageUrl;
|
||||
|
||||
// Add additional images (excluding the main image)
|
||||
final additionalImages = imageUrls.where((url) => url != imageUrl).toList();
|
||||
if (additionalImages.isNotEmpty) {
|
||||
data['foto_aset_tambahan'] = additionalImages;
|
||||
}
|
||||
}
|
||||
|
||||
// Add rental time units if they exist
|
||||
if (satuanWaktuSewa.isNotEmpty) {
|
||||
data['satuan_waktu_sewa'] = satuanWaktuSewa.toList();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,54 +1,169 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
class PaketModel {
|
||||
final String? id;
|
||||
final String? nama;
|
||||
final String? deskripsi;
|
||||
final int? harga;
|
||||
final int? kuantitas;
|
||||
final String? foto_paket;
|
||||
final List<dynamic>? satuanWaktuSewa;
|
||||
|
||||
final String id;
|
||||
final String nama;
|
||||
final String deskripsi;
|
||||
final double harga;
|
||||
final int kuantitas;
|
||||
final String status;
|
||||
List<String> foto;
|
||||
List<Map<String, dynamic>> satuanWaktuSewa;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
String? foto_paket; // Main photo URL
|
||||
List<String>? images; // List of photo URLs
|
||||
|
||||
PaketModel({
|
||||
this.id,
|
||||
this.nama,
|
||||
this.deskripsi,
|
||||
this.harga,
|
||||
this.kuantitas,
|
||||
required this.id,
|
||||
required this.nama,
|
||||
required this.deskripsi,
|
||||
required this.harga,
|
||||
required this.kuantitas,
|
||||
this.status = 'aktif',
|
||||
required List<String> foto,
|
||||
required List<Map<String, dynamic>> satuanWaktuSewa,
|
||||
this.foto_paket,
|
||||
this.satuanWaktuSewa,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'nama': nama,
|
||||
'deskripsi': deskripsi,
|
||||
'harga': harga,
|
||||
'kuantitas': kuantitas,
|
||||
'foto_paket': foto_paket,
|
||||
'satuanWaktuSewa': satuanWaktuSewa,
|
||||
};
|
||||
}
|
||||
|
||||
factory PaketModel.fromMap(Map<String, dynamic> map) {
|
||||
List<String>? images,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
}) : foto = List.from(foto),
|
||||
satuanWaktuSewa = List.from(satuanWaktuSewa),
|
||||
images = images != null ? List.from(images) : [];
|
||||
|
||||
// Add copyWith method for immutability patterns
|
||||
PaketModel copyWith({
|
||||
String? id,
|
||||
String? nama,
|
||||
String? deskripsi,
|
||||
double? harga,
|
||||
int? kuantitas,
|
||||
String? status,
|
||||
List<String>? foto,
|
||||
List<Map<String, dynamic>>? satuanWaktuSewa,
|
||||
String? foto_paket,
|
||||
List<String>? images,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return PaketModel(
|
||||
id: map['id'],
|
||||
nama: map['nama'],
|
||||
deskripsi: map['deskripsi'],
|
||||
harga: map['harga']?.toInt(),
|
||||
kuantitas: map['kuantitas']?.toInt(),
|
||||
foto_paket: map['foto_paket'],
|
||||
satuanWaktuSewa: map['satuanWaktuSewa'],
|
||||
id: id ?? this.id,
|
||||
nama: nama ?? this.nama,
|
||||
deskripsi: deskripsi ?? this.deskripsi,
|
||||
harga: harga ?? this.harga,
|
||||
kuantitas: kuantitas ?? this.kuantitas,
|
||||
status: status ?? this.status,
|
||||
foto: foto ?? List.from(this.foto),
|
||||
satuanWaktuSewa: satuanWaktuSewa ?? List.from(this.satuanWaktuSewa),
|
||||
foto_paket: foto_paket ?? this.foto_paket,
|
||||
images: images ?? (this.images != null ? List.from(this.images!) : null),
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
// Alias for fromJson to maintain compatibility
|
||||
factory PaketModel.fromMap(Map<String, dynamic> json) => PaketModel.fromJson(json);
|
||||
|
||||
factory PaketModel.fromJson(Map<String, dynamic> json) {
|
||||
// Handle different possible JSON structures
|
||||
final fotoList = <String>[];
|
||||
|
||||
// Check for different possible photo field names
|
||||
if (json['foto'] != null) {
|
||||
if (json['foto'] is String) {
|
||||
fotoList.add(json['foto']);
|
||||
} else if (json['foto'] is List) {
|
||||
fotoList.addAll((json['foto'] as List).whereType<String>());
|
||||
}
|
||||
}
|
||||
|
||||
if (json['foto_paket'] != null) {
|
||||
if (json['foto_paket'] is String) {
|
||||
fotoList.add(json['foto_paket']);
|
||||
} else if (json['foto_paket'] is List) {
|
||||
fotoList.addAll((json['foto_paket'] as List).whereType<String>());
|
||||
}
|
||||
}
|
||||
|
||||
// Handle satuan_waktu_sewa
|
||||
List<Map<String, dynamic>> satuanWaktuList = [];
|
||||
if (json['satuan_waktu_sewa'] != null) {
|
||||
if (json['satuan_waktu_sewa'] is List) {
|
||||
satuanWaktuList = List<Map<String, dynamic>>.from(
|
||||
json['satuan_waktu_sewa'].map((x) => x is Map ? Map<String, dynamic>.from(x) : {})
|
||||
);
|
||||
} else if (json['satuan_waktu_sewa'] is Map) {
|
||||
satuanWaktuList = [Map<String, dynamic>.from(json['satuan_waktu_sewa'])];
|
||||
}
|
||||
}
|
||||
|
||||
developer.log('📦 [PaketModel.fromJson] Raw status: ${json['status']} (type: ${json['status']?.runtimeType})');
|
||||
final status = json['status']?.toString().toLowerCase() ?? 'aktif';
|
||||
developer.log(' 🏷️ Processed status: $status');
|
||||
|
||||
return PaketModel(
|
||||
id: json['id']?.toString() ?? '',
|
||||
nama: json['nama']?.toString() ?? '',
|
||||
deskripsi: json['deskripsi']?.toString() ?? '',
|
||||
status: status,
|
||||
harga: (json['harga'] is num) ? (json['harga'] as num).toDouble() : 0.0,
|
||||
kuantitas: (json['kuantitas'] is num) ? (json['kuantitas'] as num).toInt() : 1,
|
||||
foto: fotoList,
|
||||
satuanWaktuSewa: satuanWaktuList,
|
||||
foto_paket: json['foto_paket']?.toString(),
|
||||
images: json['images'] != null ? List<String>.from(json['images']) : null,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'].toString())
|
||||
: DateTime.now(),
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'].toString())
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
factory PaketModel.fromJson(String source) => PaketModel.fromMap(json.decode(source));
|
||||
// Convert to JSON
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'nama': nama,
|
||||
'deskripsi': deskripsi,
|
||||
'harga': harga,
|
||||
'kuantitas': kuantitas,
|
||||
'foto': foto,
|
||||
'foto_paket': foto_paket,
|
||||
'images': images,
|
||||
'satuan_waktu_sewa': satuanWaktuSewa,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PaketModel(id: $id, nama: $nama, deskripsi: $deskripsi, harga: $harga, kuantitas: $kuantitas, foto_paket: $foto_paket, satuanWaktuSewa: $satuanWaktuSewa)';
|
||||
|
||||
// Get the first photo URL or a placeholder
|
||||
String get firstPhotoUrl => foto.isNotEmpty ? foto.first : '';
|
||||
|
||||
// Get the formatted price
|
||||
String get formattedPrice => 'Rp${harga.toStringAsFixed(0).replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]}.',
|
||||
)}';
|
||||
|
||||
// Check if the package is available
|
||||
bool get isAvailable => kuantitas > 0;
|
||||
|
||||
// Get the first available time unit
|
||||
Map<String, dynamic>? get defaultTimeUnit =>
|
||||
satuanWaktuSewa.isNotEmpty ? satuanWaktuSewa.first : null;
|
||||
|
||||
// Get the price for a specific time unit
|
||||
double getPriceForTimeUnit(String timeUnitId) {
|
||||
try {
|
||||
final unit = satuanWaktuSewa.firstWhere(
|
||||
(unit) => unit['id'] == timeUnitId || unit['id'].toString() == timeUnitId,
|
||||
);
|
||||
return (unit['harga'] as num?)?.toDouble() ?? 0.0;
|
||||
} catch (e) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
lib/app/data/models/pembayaran_model.dart
Normal file
@ -0,0 +1,22 @@
|
||||
class PembayaranModel {
|
||||
final String id;
|
||||
final int totalPembayaran;
|
||||
final String metodePembayaran;
|
||||
final DateTime waktuPembayaran;
|
||||
|
||||
PembayaranModel({
|
||||
required this.id,
|
||||
required this.totalPembayaran,
|
||||
required this.metodePembayaran,
|
||||
required this.waktuPembayaran,
|
||||
});
|
||||
|
||||
factory PembayaranModel.fromJson(Map<String, dynamic> json) {
|
||||
return PembayaranModel(
|
||||
id: json['id'] as String,
|
||||
totalPembayaran: json['total_pembayaran'] as int,
|
||||
metodePembayaran: json['metode_pembayaran'] as String,
|
||||
waktuPembayaran: DateTime.parse(json['waktu_pembayaran'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1 +1,99 @@
|
||||
class SewaModel {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String status;
|
||||
final DateTime waktuMulai;
|
||||
final DateTime waktuSelesai;
|
||||
final DateTime tanggalPemesanan;
|
||||
final String tipePesanan;
|
||||
final int kuantitas;
|
||||
// Untuk tunggal
|
||||
final String? asetId;
|
||||
final String? asetNama;
|
||||
final String? asetFoto;
|
||||
// Untuk paket
|
||||
final String? paketId;
|
||||
final String? paketNama;
|
||||
final String? paketFoto;
|
||||
// Tagihan
|
||||
final double totalTagihan;
|
||||
// Data warga
|
||||
final String wargaNama;
|
||||
final String wargaNoHp;
|
||||
final String wargaAvatar;
|
||||
final double? denda;
|
||||
final double? dibayar;
|
||||
final double? paidAmount;
|
||||
// Add nama_satuan_waktu field
|
||||
final String? namaSatuanWaktu;
|
||||
|
||||
SewaModel({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.status,
|
||||
required this.waktuMulai,
|
||||
required this.waktuSelesai,
|
||||
required this.tanggalPemesanan,
|
||||
required this.tipePesanan,
|
||||
required this.kuantitas,
|
||||
this.asetId,
|
||||
this.asetNama,
|
||||
this.asetFoto,
|
||||
this.paketId,
|
||||
this.paketNama,
|
||||
this.paketFoto,
|
||||
required this.totalTagihan,
|
||||
required this.wargaNama,
|
||||
required this.wargaNoHp,
|
||||
required this.wargaAvatar,
|
||||
this.denda,
|
||||
this.dibayar,
|
||||
this.paidAmount,
|
||||
this.namaSatuanWaktu,
|
||||
});
|
||||
|
||||
factory SewaModel.fromJson(Map<String, dynamic> json) {
|
||||
return SewaModel(
|
||||
id: json['id'] ?? '',
|
||||
userId: json['user_id'] ?? '',
|
||||
status: json['status'] ?? '',
|
||||
waktuMulai: DateTime.parse(
|
||||
json['waktu_mulai'] ?? DateTime.now().toIso8601String(),
|
||||
),
|
||||
waktuSelesai: DateTime.parse(
|
||||
json['waktu_selesai'] ?? DateTime.now().toIso8601String(),
|
||||
),
|
||||
tanggalPemesanan: DateTime.parse(
|
||||
json['tanggal_pemesanan'] ?? DateTime.now().toIso8601String(),
|
||||
),
|
||||
tipePesanan: json['tipe_pesanan'] ?? '',
|
||||
kuantitas: json['kuantitas'] ?? 1,
|
||||
asetId: json['aset_id'],
|
||||
asetNama: json['aset_nama'],
|
||||
asetFoto: json['aset_foto'],
|
||||
paketId: json['paket_id'],
|
||||
paketNama: json['paket_nama'],
|
||||
paketFoto: json['paket_foto'],
|
||||
totalTagihan:
|
||||
(json['total_tagihan'] is num)
|
||||
? json['total_tagihan'].toDouble()
|
||||
: double.tryParse(json['total_tagihan']?.toString() ?? '0') ?? 0,
|
||||
wargaNama: json['warga_nama'] ?? '',
|
||||
wargaNoHp: json['warga_no_hp'] ?? '',
|
||||
wargaAvatar: json['warga_avatar'] ?? '',
|
||||
denda:
|
||||
(json['denda'] is num)
|
||||
? json['denda'].toDouble()
|
||||
: double.tryParse(json['denda']?.toString() ?? '0'),
|
||||
dibayar:
|
||||
(json['dibayar'] is num)
|
||||
? json['dibayar'].toDouble()
|
||||
: double.tryParse(json['dibayar']?.toString() ?? '0'),
|
||||
paidAmount:
|
||||
(json['paid_amount'] is num)
|
||||
? json['paid_amount'].toDouble()
|
||||
: double.tryParse(json['paid_amount']?.toString() ?? '0'),
|
||||
namaSatuanWaktu: json['nama_satuan_waktu'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,6 +75,25 @@ class AuthProvider extends GetxService {
|
||||
await client.auth.signOut();
|
||||
}
|
||||
|
||||
// Method to clear any cached data in the AuthProvider
|
||||
void clearAuthData() {
|
||||
// Clear any cached user data or state
|
||||
// This method is called during logout to ensure all user-related data is cleared
|
||||
debugPrint('Clearing AuthProvider cached data');
|
||||
|
||||
// Explicitly clear any cached data that might be stored in the provider
|
||||
// This is important to ensure no user data remains after logout or registration
|
||||
try {
|
||||
// Force refresh of the auth state
|
||||
client.auth.refreshSession();
|
||||
|
||||
// Log the cleanup action
|
||||
debugPrint('AuthProvider cached data cleared successfully');
|
||||
} catch (e) {
|
||||
debugPrint('Error clearing AuthProvider cached data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
User? get currentUser => client.auth.currentUser;
|
||||
|
||||
Stream<AuthState> get authChanges => client.auth.onAuthStateChange;
|
||||
@ -415,28 +434,17 @@ class AuthProvider extends GetxService {
|
||||
final userData =
|
||||
await client
|
||||
.from('warga_desa')
|
||||
.select('nomor_telepon, no_telepon, phone')
|
||||
.select('no_hp')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
// Jika berhasil mendapatkan data, cek beberapa kemungkinan nama kolom
|
||||
if (userData != null) {
|
||||
if (userData.containsKey('nomor_telepon')) {
|
||||
final phone = userData['nomor_telepon']?.toString();
|
||||
if (phone != null && phone.isNotEmpty) return phone;
|
||||
}
|
||||
|
||||
if (userData.containsKey('no_telepon')) {
|
||||
final phone = userData['no_telepon']?.toString();
|
||||
if (phone != null && phone.isNotEmpty) return phone;
|
||||
}
|
||||
|
||||
if (userData.containsKey('phone')) {
|
||||
final phone = userData['phone']?.toString();
|
||||
if (userData.containsKey('no_hp')) {
|
||||
final phone = userData['no_hp']?.toString();
|
||||
if (phone != null && phone.isNotEmpty) return phone;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback ke data dari Supabase Auth
|
||||
final userMetadata = user.userMetadata;
|
||||
if (userMetadata != null) {
|
||||
@ -496,6 +504,146 @@ class AuthProvider extends GetxService {
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk mendapatkan tanggal lahir dari tabel warga_desa berdasarkan user_id
|
||||
Future<String?> getUserTanggalLahir() async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
debugPrint('No current user found when getting tanggal_lahir');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('Fetching tanggal_lahir for user_id: ${user.id}');
|
||||
|
||||
// Coba ambil tanggal lahir dari tabel warga_desa
|
||||
final userData =
|
||||
await client
|
||||
.from('warga_desa')
|
||||
.select('tanggal_lahir')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
// Jika berhasil mendapatkan data
|
||||
if (userData != null && userData.containsKey('tanggal_lahir')) {
|
||||
final tanggalLahir = userData['tanggal_lahir']?.toString();
|
||||
if (tanggalLahir != null && tanggalLahir.isNotEmpty) {
|
||||
debugPrint('Found tanggal_lahir: $tanggalLahir');
|
||||
return tanggalLahir;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching user tanggal_lahir: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk mendapatkan RT/RW dari tabel warga_desa berdasarkan user_id
|
||||
Future<String?> getUserRtRw() async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
debugPrint('No current user found when getting rt_rw');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('Fetching rt_rw for user_id: ${user.id}');
|
||||
|
||||
// Coba ambil RT/RW dari tabel warga_desa
|
||||
final userData =
|
||||
await client
|
||||
.from('warga_desa')
|
||||
.select('rt_rw')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
// Jika berhasil mendapatkan data
|
||||
if (userData != null && userData.containsKey('rt_rw')) {
|
||||
final rtRw = userData['rt_rw']?.toString();
|
||||
if (rtRw != null && rtRw.isNotEmpty) {
|
||||
debugPrint('Found rt_rw: $rtRw');
|
||||
return rtRw;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching user rt_rw: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk mendapatkan kelurahan/desa dari tabel warga_desa berdasarkan user_id
|
||||
Future<String?> getUserKelurahanDesa() async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
debugPrint('No current user found when getting kelurahan_desa');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('Fetching kelurahan_desa for user_id: ${user.id}');
|
||||
|
||||
// Coba ambil kelurahan/desa dari tabel warga_desa
|
||||
final userData =
|
||||
await client
|
||||
.from('warga_desa')
|
||||
.select('kelurahan_desa')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
// Jika berhasil mendapatkan data
|
||||
if (userData != null && userData.containsKey('kelurahan_desa')) {
|
||||
final kelurahanDesa = userData['kelurahan_desa']?.toString();
|
||||
if (kelurahanDesa != null && kelurahanDesa.isNotEmpty) {
|
||||
debugPrint('Found kelurahan_desa: $kelurahanDesa');
|
||||
return kelurahanDesa;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching user kelurahan_desa: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk mendapatkan kecamatan dari tabel warga_desa berdasarkan user_id
|
||||
Future<String?> getUserKecamatan() async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
debugPrint('No current user found when getting kecamatan');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('Fetching kecamatan for user_id: ${user.id}');
|
||||
|
||||
// Coba ambil kecamatan dari tabel warga_desa
|
||||
final userData =
|
||||
await client
|
||||
.from('warga_desa')
|
||||
.select('kecamatan')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
// Jika berhasil mendapatkan data
|
||||
if (userData != null && userData.containsKey('kecamatan')) {
|
||||
final kecamatan = userData['kecamatan']?.toString();
|
||||
if (kecamatan != null && kecamatan.isNotEmpty) {
|
||||
debugPrint('Found kecamatan: $kecamatan');
|
||||
return kecamatan;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching user kecamatan: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Mendapatkan data sewa_aset berdasarkan status (misal: MENUNGGU PEMBAYARAN, PEMBAYARANAN DENDA)
|
||||
Future<List<Map<String, dynamic>>> getSewaAsetByStatus(
|
||||
List<String> statuses,
|
||||
@ -507,28 +655,97 @@ class AuthProvider extends GetxService {
|
||||
}
|
||||
try {
|
||||
debugPrint(
|
||||
'Fetching sewa_aset for user_id: \\${user.id} with statuses: \\${statuses.join(', ')}',
|
||||
'Fetching sewa_aset for user_id: ${user.id} with statuses: ${statuses.join(', ')}',
|
||||
);
|
||||
// Supabase expects the IN filter as a comma-separated string in parentheses
|
||||
final statusString = '(${statuses.map((s) => '"$s"').join(',')})';
|
||||
|
||||
// Get sewa_aset records filtered by user_id and status
|
||||
final response = await client
|
||||
.from('sewa_aset')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.filter('status', 'in', statusString);
|
||||
debugPrint('Fetched sewa_aset count: \\${response.length}');
|
||||
// Pastikan response adalah List
|
||||
.filter('status', 'in', statusString)
|
||||
.order('created_at', ascending: false);
|
||||
|
||||
debugPrint('Fetched sewa_aset count: ${response.length}');
|
||||
|
||||
// Process the response to handle package data
|
||||
if (response is List) {
|
||||
return response
|
||||
.map<Map<String, dynamic>>(
|
||||
(item) => Map<String, dynamic>.from(item),
|
||||
)
|
||||
.toList();
|
||||
final List<Map<String, dynamic>> processedResponse = [];
|
||||
|
||||
for (var item in response) {
|
||||
final Map<String, dynamic> processedItem = Map<String, dynamic>.from(
|
||||
item,
|
||||
);
|
||||
|
||||
// Ensure updated_at is not null, use created_at as fallback
|
||||
if (processedItem['updated_at'] == null &&
|
||||
processedItem['created_at'] != null) {
|
||||
debugPrint('updated_at is null, using created_at as fallback');
|
||||
processedItem['updated_at'] = processedItem['created_at'];
|
||||
} else if (processedItem['updated_at'] == null &&
|
||||
processedItem['created_at'] == null) {
|
||||
// If both are null, use current timestamp as last resort
|
||||
debugPrint(
|
||||
'Both updated_at and created_at are null, using current timestamp',
|
||||
);
|
||||
processedItem['updated_at'] = DateTime.now().toIso8601String();
|
||||
}
|
||||
|
||||
// Debug the updated_at field
|
||||
debugPrint(
|
||||
'updated_at after processing: ${processedItem['updated_at']}',
|
||||
);
|
||||
|
||||
// If aset_id is null and paket_id is not null, fetch package data
|
||||
if (item['aset_id'] == null && item['paket_id'] != null) {
|
||||
final String paketId = item['paket_id'];
|
||||
debugPrint(
|
||||
'Found rental with paket_id: $paketId, fetching package details',
|
||||
);
|
||||
|
||||
try {
|
||||
// Get package name from paket table
|
||||
final paketResponse =
|
||||
await client
|
||||
.from('paket')
|
||||
.select('nama')
|
||||
.eq('id', paketId)
|
||||
.maybeSingle();
|
||||
|
||||
if (paketResponse != null && paketResponse['nama'] != null) {
|
||||
processedItem['nama_paket'] = paketResponse['nama'];
|
||||
debugPrint('Found package name: ${paketResponse['nama']}');
|
||||
}
|
||||
|
||||
// Get package photo from foto_aset table
|
||||
final fotoResponse =
|
||||
await client
|
||||
.from('foto_aset')
|
||||
.select('foto_aset')
|
||||
.eq('id_paket', paketId)
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
if (fotoResponse != null && fotoResponse['foto_aset'] != null) {
|
||||
processedItem['foto_paket'] = fotoResponse['foto_aset'];
|
||||
debugPrint('Found package photo: ${fotoResponse['foto_aset']}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching package details: $e');
|
||||
}
|
||||
}
|
||||
|
||||
processedResponse.add(processedItem);
|
||||
}
|
||||
|
||||
return processedResponse;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching sewa_aset by status: \\${e.toString()}');
|
||||
debugPrint('Error fetching sewa_aset by status: ${e.toString()}');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,16 @@ class PesananProvider {
|
||||
final SupabaseClient _supabase = Supabase.instance.client;
|
||||
final _tableName = 'pesanan';
|
||||
|
||||
// Method to clear any cached data
|
||||
void clearCache() {
|
||||
print('Clearing PesananProvider cached data');
|
||||
// Clear any cached order data or state
|
||||
// This is useful when logging out to ensure no user data remains in memory
|
||||
|
||||
// Note: Since this provider doesn't currently maintain any persistent cache variables,
|
||||
// this method serves as a placeholder for future cache implementations
|
||||
}
|
||||
|
||||
Future<List<PesananModel>> getPesananByUserId(String userId) async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
|
||||
10
lib/app/main.dart
Normal file
@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||
import 'package:bumrent_app/main.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
Get.put(AsetProvider());
|
||||
runApp(const MyApp());
|
||||
}
|
||||
@ -2,12 +2,18 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import 'dart:math';
|
||||
import '../../../modules/warga/controllers/warga_dashboard_controller.dart';
|
||||
|
||||
class AuthController extends GetxController {
|
||||
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
||||
|
||||
final emailController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final nameController = TextEditingController();
|
||||
final confirmPasswordController = TextEditingController();
|
||||
final RxBool isConfirmPasswordVisible = false.obs;
|
||||
|
||||
// Form fields for registration
|
||||
final RxString email = ''.obs;
|
||||
@ -15,6 +21,11 @@ class AuthController extends GetxController {
|
||||
final RxString nik = ''.obs;
|
||||
final RxString phoneNumber = ''.obs;
|
||||
final RxString selectedRole = 'WARGA'.obs; // Default role
|
||||
final RxString alamatLengkap = ''.obs;
|
||||
final Rx<DateTime?> tanggalLahir = Rx<DateTime?>(null);
|
||||
final RxString rtRw = ''.obs;
|
||||
final RxString kelurahan = ''.obs;
|
||||
final RxString kecamatan = ''.obs;
|
||||
|
||||
// Form status
|
||||
final RxBool isLoading = false.obs;
|
||||
@ -28,6 +39,10 @@ class AuthController extends GetxController {
|
||||
isPasswordVisible.value = !isPasswordVisible.value;
|
||||
}
|
||||
|
||||
void toggleConfirmPasswordVisibility() {
|
||||
isConfirmPasswordVisible.value = !isConfirmPasswordVisible.value;
|
||||
}
|
||||
|
||||
// Change role selection
|
||||
void setRole(String? role) {
|
||||
if (role != null) {
|
||||
@ -87,7 +102,7 @@ class AuthController extends GetxController {
|
||||
|
||||
// Navigate based on role name
|
||||
if (roleName == null) {
|
||||
_navigateToWargaDashboard(); // Default to warga if role name not found
|
||||
await _checkWargaStatusAndNavigate(); // Default to warga if role name not found
|
||||
return;
|
||||
}
|
||||
|
||||
@ -96,6 +111,9 @@ class AuthController extends GetxController {
|
||||
_navigateToPetugasBumdesDashboard();
|
||||
break;
|
||||
case 'WARGA':
|
||||
// For WARGA role, check account status in warga_desa table
|
||||
await _checkWargaStatusAndNavigate();
|
||||
break;
|
||||
default:
|
||||
_navigateToWargaDashboard();
|
||||
break;
|
||||
@ -105,12 +123,72 @@ class AuthController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// Check warga status in warga_desa table and navigate accordingly
|
||||
Future<void> _checkWargaStatusAndNavigate() async {
|
||||
try {
|
||||
final user = _authProvider.currentUser;
|
||||
if (user == null) {
|
||||
errorMessage.value = 'Tidak dapat memperoleh data pengguna';
|
||||
return;
|
||||
}
|
||||
|
||||
// Get user data from warga_desa table
|
||||
final userData =
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.select('status, keterangan')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (userData == null) {
|
||||
errorMessage.value = 'Data pengguna tidak ditemukan';
|
||||
return;
|
||||
}
|
||||
|
||||
final status = userData['status'] as String?;
|
||||
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'active':
|
||||
// Allow login for active users
|
||||
_navigateToWargaDashboard();
|
||||
break;
|
||||
case 'suspended':
|
||||
// Show error for suspended users
|
||||
final keterangan =
|
||||
userData['keterangan'] as String? ?? 'Tidak ada keterangan';
|
||||
errorMessage.value =
|
||||
'Akun Anda dinonaktifkan oleh petugas. Keterangan: $keterangan';
|
||||
// Sign out the user
|
||||
await _authProvider.signOut();
|
||||
break;
|
||||
case 'pending':
|
||||
// Show error for pending users
|
||||
errorMessage.value =
|
||||
'Akun Anda sedang dalam proses verifikasi. Silakan tunggu hingga verifikasi selesai.';
|
||||
// Sign out the user
|
||||
await _authProvider.signOut();
|
||||
break;
|
||||
default:
|
||||
errorMessage.value = 'Status akun tidak valid';
|
||||
// Sign out the user
|
||||
await _authProvider.signOut();
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Gagal memeriksa status akun: ${e.toString()}';
|
||||
// Sign out the user on error
|
||||
await _authProvider.signOut();
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToPetugasBumdesDashboard() {
|
||||
Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
|
||||
}
|
||||
|
||||
void _navigateToWargaDashboard() {
|
||||
Get.offAllNamed(Routes.WARGA_DASHBOARD);
|
||||
// Navigate to warga dashboard with parameter to indicate it's coming from login
|
||||
// This will trigger an immediate refresh of the data
|
||||
Get.offAllNamed(Routes.WARGA_DASHBOARD, arguments: {'from_login': true});
|
||||
}
|
||||
|
||||
void forgotPassword() async {
|
||||
@ -140,7 +218,7 @@ class AuthController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Link reset password telah dikirim ke email Anda',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green[100],
|
||||
colorText: Colors.green[800],
|
||||
icon: const Icon(Icons.check_circle, color: Colors.green),
|
||||
@ -172,63 +250,87 @@ class AuthController extends GetxController {
|
||||
void onClose() {
|
||||
emailController.dispose();
|
||||
passwordController.dispose();
|
||||
nameController.dispose();
|
||||
confirmPasswordController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Register user implementation
|
||||
Future<void> registerUser() async {
|
||||
// Validate all required fields
|
||||
if (email.value.isEmpty ||
|
||||
password.value.isEmpty ||
|
||||
nik.value.isEmpty ||
|
||||
phoneNumber.value.isEmpty) {
|
||||
errorMessage.value = 'Semua field harus diisi';
|
||||
// Clear previous error messages
|
||||
errorMessage.value = '';
|
||||
|
||||
// Validate form fields
|
||||
if (!formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation for email
|
||||
if (!GetUtils.isEmail(email.value.trim())) {
|
||||
errorMessage.value = 'Format email tidak valid';
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation for password
|
||||
if (password.value.length < 6) {
|
||||
errorMessage.value = 'Password minimal 6 karakter';
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation for NIK
|
||||
if (nik.value.length != 16) {
|
||||
errorMessage.value = 'NIK harus 16 digit';
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation for phone number
|
||||
if (!phoneNumber.value.startsWith('08') || phoneNumber.value.length < 10) {
|
||||
errorMessage.value =
|
||||
'Nomor HP tidak valid (harus diawali 08 dan minimal 10 digit)';
|
||||
// Validate date of birth separately (since it's not a standard form field)
|
||||
if (!validateDateOfBirth()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
// Create user with Supabase
|
||||
final response = await _authProvider.signUp(
|
||||
// Format tanggal lahir to string (YYYY-MM-DD)
|
||||
final formattedTanggalLahir =
|
||||
tanggalLahir.value != null
|
||||
? '${tanggalLahir.value!.year}-${tanggalLahir.value!.month.toString().padLeft(2, '0')}-${tanggalLahir.value!.day.toString().padLeft(2, '0')}'
|
||||
: '';
|
||||
|
||||
// Generate register_id with format REG-YYYY-1234567
|
||||
final currentYear = DateTime.now().year.toString();
|
||||
final randomDigits = _generateRandomDigits(7); // Generate 7 random digits
|
||||
final registerId = 'REG-$currentYear-$randomDigits';
|
||||
|
||||
// 1. Register user with Supabase Auth and add role_id to metadata
|
||||
final response = await _authProvider.client.auth.signUp(
|
||||
email: email.value.trim(),
|
||||
password: password.value,
|
||||
data: {
|
||||
'nik': nik.value.trim(),
|
||||
'phone_number': phoneNumber.value.trim(),
|
||||
'role': selectedRole.value,
|
||||
'role_id':
|
||||
'bb5360d5-8fd0-404e-8f6f-71ec4d8ad0ae', // Fixed role_id for WARGA
|
||||
},
|
||||
);
|
||||
|
||||
// Check if registration was successful
|
||||
if (response.user != null) {
|
||||
// Registration successful
|
||||
Get.offNamed(Routes.REGISTRATION_SUCCESS);
|
||||
// 2. Get the UID from the created auth user
|
||||
final userId = response.user!.id;
|
||||
|
||||
// 3. Insert user data into the warga_desa table
|
||||
await _authProvider.client.from('warga_desa').insert({
|
||||
'user_id': userId,
|
||||
'email': email.value.trim(),
|
||||
'nama_lengkap': nameController.text.trim(),
|
||||
'nik': nik.value.trim(),
|
||||
'status': 'pending',
|
||||
'tanggal_lahir': formattedTanggalLahir,
|
||||
'no_hp': phoneNumber.value.trim(),
|
||||
'rt_rw': rtRw.value.trim(),
|
||||
'kelurahan_desa': kelurahan.value.trim(),
|
||||
'kecamatan': kecamatan.value.trim(),
|
||||
'alamat': alamatLengkap.value.trim(),
|
||||
'register_id': registerId, // Add register_id to the warga_desa table
|
||||
});
|
||||
|
||||
// Reset registration fields BEFORE navigation to ensure clean state
|
||||
resetRegistrationFields();
|
||||
|
||||
// Bersihkan data auth provider untuk memastikan tidak ada data user yang tersimpan
|
||||
_authProvider.clearAuthData();
|
||||
|
||||
// Print debug message
|
||||
print(
|
||||
'Registration successful: Fields and controllers have been cleared',
|
||||
);
|
||||
|
||||
// Registration successful - navigate to success page
|
||||
Get.offNamed(
|
||||
Routes.REGISTRATION_SUCCESS,
|
||||
arguments: {'register_id': registerId},
|
||||
);
|
||||
} else {
|
||||
errorMessage.value = 'Gagal mendaftar. Silakan coba lagi.';
|
||||
}
|
||||
@ -239,4 +341,250 @@ class AuthController extends GetxController {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate random digits of specified length
|
||||
String _generateRandomDigits(int length) {
|
||||
final random = Random();
|
||||
final buffer = StringBuffer();
|
||||
for (var i = 0; i < length; i++) {
|
||||
buffer.write(random.nextInt(10));
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
// Validation methods
|
||||
String? validateEmail(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Email tidak boleh kosong';
|
||||
}
|
||||
if (!GetUtils.isEmail(value)) {
|
||||
return 'Format email tidak valid';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validatePassword(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Password tidak boleh kosong';
|
||||
}
|
||||
if (value.length < 8) {
|
||||
return 'Password minimal 8 karakter';
|
||||
}
|
||||
if (!value.contains(RegExp(r'[A-Z]'))) {
|
||||
return 'Password harus memiliki minimal 1 huruf besar';
|
||||
}
|
||||
if (!value.contains(RegExp(r'[0-9]'))) {
|
||||
return 'Password harus memiliki minimal 1 angka';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateConfirmPassword(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Konfirmasi password tidak boleh kosong';
|
||||
}
|
||||
if (value != password.value) {
|
||||
return 'Password tidak cocok';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateName(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Nama lengkap tidak boleh kosong';
|
||||
}
|
||||
if (value.length < 3) {
|
||||
return 'Nama lengkap minimal 3 karakter';
|
||||
}
|
||||
if (!RegExp(r"^[a-zA-Z\s\.]+$").hasMatch(value)) {
|
||||
return 'Nama hanya boleh berisi huruf, spasi, titik, dan apostrof';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateNIK(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'NIK tidak boleh kosong';
|
||||
}
|
||||
if (value.length != 16) {
|
||||
return 'NIK harus 16 digit';
|
||||
}
|
||||
if (!RegExp(r'^[0-9]+$').hasMatch(value)) {
|
||||
return 'NIK hanya boleh berisi angka';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validatePhone(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'No HP tidak boleh kosong';
|
||||
}
|
||||
if (!value.startsWith('08')) {
|
||||
return 'Nomor HP harus diawali dengan 08';
|
||||
}
|
||||
if (value.length < 10 || value.length > 13) {
|
||||
return 'Nomor HP harus antara 10-13 digit';
|
||||
}
|
||||
if (!RegExp(r'^[0-9]+$').hasMatch(value)) {
|
||||
return 'Nomor HP hanya boleh berisi angka';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateRTRW(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'RT/RW tidak boleh kosong';
|
||||
}
|
||||
if (!RegExp(r'^\d{1,3}\/\d{1,3}$').hasMatch(value)) {
|
||||
return 'Format RT/RW tidak valid (contoh: 001/002)';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateKelurahan(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Kelurahan/Desa tidak boleh kosong';
|
||||
}
|
||||
if (value.length < 3) {
|
||||
return 'Kelurahan/Desa minimal 3 karakter';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateKecamatan(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Kecamatan tidak boleh kosong';
|
||||
}
|
||||
if (value.length < 3) {
|
||||
return 'Kecamatan minimal 3 karakter';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateAlamat(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Alamat lengkap tidak boleh kosong';
|
||||
}
|
||||
if (value.length < 5) {
|
||||
return 'Alamat terlalu pendek, minimal 5 karakter';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool validateDateOfBirth() {
|
||||
if (tanggalLahir.value == null) {
|
||||
errorMessage.value = 'Tanggal lahir harus diisi';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if user is at least 17 years old
|
||||
final DateTime today = DateTime.now();
|
||||
final DateTime minimumAge = DateTime(
|
||||
today.year - 17,
|
||||
today.month,
|
||||
today.day,
|
||||
);
|
||||
|
||||
if (tanggalLahir.value!.isAfter(minimumAge)) {
|
||||
errorMessage.value = 'Anda harus berusia minimal 17 tahun';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check registration status by register_id and email
|
||||
Future<Map<String, dynamic>?> checkRegistrationStatus(
|
||||
String registerId,
|
||||
String email,
|
||||
) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
print('Checking registration status - ID: $registerId, Email: $email');
|
||||
|
||||
// Validasi input
|
||||
if (registerId.isEmpty || email.isEmpty) {
|
||||
print('Invalid input: registerId or email is empty');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Query warga_desa table where register_id and email match
|
||||
final response =
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.select(
|
||||
'*',
|
||||
) // Ensure we select all columns including 'keterangan'
|
||||
.eq('register_id', registerId)
|
||||
.eq('email', email)
|
||||
.maybeSingle();
|
||||
|
||||
// Log response for debugging
|
||||
print('Registration status query response: $response');
|
||||
|
||||
// Validasi hasil query
|
||||
if (response == null) {
|
||||
print('No matching registration found');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response is Map<String, dynamic> && response.isEmpty) {
|
||||
print('Empty response received');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Jika berhasil, kembalikan data
|
||||
print('Registration found with status: ${response['status']}');
|
||||
return response;
|
||||
} catch (e) {
|
||||
print('Error checking registration status: ${e.toString()}');
|
||||
return null;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset all registration fields
|
||||
void resetRegistrationFields() {
|
||||
// Reset text controllers
|
||||
emailController.clear();
|
||||
passwordController.clear();
|
||||
nameController.clear();
|
||||
confirmPasswordController.clear();
|
||||
|
||||
// Reset form fields
|
||||
email.value = '';
|
||||
password.value = '';
|
||||
nik.value = '';
|
||||
phoneNumber.value = '';
|
||||
selectedRole.value = 'WARGA'; // Reset to default role
|
||||
alamatLengkap.value = '';
|
||||
tanggalLahir.value = null;
|
||||
rtRw.value = '';
|
||||
kelurahan.value = '';
|
||||
kecamatan.value = '';
|
||||
|
||||
// Reset form status
|
||||
isPasswordVisible.value = false;
|
||||
isConfirmPasswordVisible.value = false;
|
||||
errorMessage.value = '';
|
||||
|
||||
// Reset form key if needed
|
||||
if (formKey.currentState != null) {
|
||||
formKey.currentState!.reset();
|
||||
}
|
||||
|
||||
// Bersihkan WargaDashboardController jika terdaftar
|
||||
try {
|
||||
if (Get.isRegistered<WargaDashboardController>()) {
|
||||
print(
|
||||
'Removing WargaDashboardController to ensure clean state after registration',
|
||||
);
|
||||
Get.delete<WargaDashboardController>(force: true);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error removing WargaDashboardController: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,12 +26,8 @@ class ForgotPasswordView extends GetView<AuthController> {
|
||||
Opacity(
|
||||
opacity: 0.03,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/pattern.png'),
|
||||
repeat: ImageRepeat.repeat,
|
||||
scale: 4.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[50], // Temporary solid color
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -9,6 +9,7 @@ class LoginView extends GetView<AuthController> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Stack(
|
||||
children: [
|
||||
// Background gradient
|
||||
@ -30,12 +31,8 @@ class LoginView extends GetView<AuthController> {
|
||||
Opacity(
|
||||
opacity: 0.03,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/pattern.png'),
|
||||
repeat: ImageRepeat.repeat,
|
||||
scale: 4.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[50], // Temporary solid color
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -76,20 +73,19 @@ class LoginView extends GetView<AuthController> {
|
||||
),
|
||||
),
|
||||
|
||||
// Main content
|
||||
// Main content with keyboard avoidance
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 50),
|
||||
SizedBox(height: MediaQuery.of(context).size.height * 0.05),
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 40),
|
||||
SizedBox(height: MediaQuery.of(context).size.height * 0.03),
|
||||
_buildLoginCard(),
|
||||
const SizedBox(height: 24),
|
||||
_buildRegisterLink(),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
@ -108,12 +104,12 @@ class LoginView extends GetView<AuthController> {
|
||||
tag: 'logo',
|
||||
child: Image.asset(
|
||||
'assets/images/logo.png',
|
||||
width: 220,
|
||||
height: 220,
|
||||
width: 180,
|
||||
height: 180,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Icon(
|
||||
Icons.apartment_rounded,
|
||||
size: 180,
|
||||
size: 150,
|
||||
color: AppColors.primary,
|
||||
);
|
||||
},
|
||||
@ -128,7 +124,7 @@ class LoginView extends GetView<AuthController> {
|
||||
shadowColor: AppColors.shadow,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(28.0),
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -150,7 +146,7 @@ class LoginView extends GetView<AuthController> {
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Email field
|
||||
_buildInputLabel('Email'),
|
||||
@ -161,7 +157,7 @@ class LoginView extends GetView<AuthController> {
|
||||
prefixIcon: Icons.email_outlined,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Password field
|
||||
_buildInputLabel('Password'),
|
||||
@ -204,13 +200,12 @@ class LoginView extends GetView<AuthController> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Login button
|
||||
Obx(
|
||||
() => SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
height: 50, // Slightly smaller height
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
controller.isLoading.value ? null : controller.login,
|
||||
@ -315,6 +310,16 @@ class LoginView extends GetView<AuthController> {
|
||||
keyboardType: keyboardType,
|
||||
obscureText: obscureText,
|
||||
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
||||
textInputAction:
|
||||
keyboardType == TextInputType.emailAddress
|
||||
? TextInputAction.next
|
||||
: TextInputAction.done,
|
||||
scrollPhysics: const ClampingScrollPhysics(),
|
||||
onChanged: (_) {
|
||||
if (controller.text.isNotEmpty) {
|
||||
this.controller.errorMessage.value = '';
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: TextStyle(color: AppColors.textLight),
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class RegistrationSuccessView extends StatefulWidget {
|
||||
const RegistrationSuccessView({Key? key}) : super(key: key);
|
||||
@ -15,10 +18,17 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
String? registerId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Get the registration ID from arguments
|
||||
if (Get.arguments != null && Get.arguments is Map) {
|
||||
registerId = Get.arguments['register_id'] as String?;
|
||||
}
|
||||
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
@ -215,7 +225,7 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
'Akun Anda telah berhasil terdaftar. Silakan masuk dengan email dan password yang telah Anda daftarkan.',
|
||||
'Akun Anda telah berhasil terdaftar. Silahkan tunggu petugas untuk melakukan verifikasi data diri anda.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.textSecondary,
|
||||
@ -224,6 +234,84 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
if (registerId != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Kode Registrasi:',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.successLight,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppColors.success.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
registerId!,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.success,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.copy,
|
||||
size: 20,
|
||||
color: AppColors.success,
|
||||
),
|
||||
onPressed: () {
|
||||
// Copy to clipboard
|
||||
final data = ClipboardData(text: registerId!);
|
||||
Clipboard.setData(data);
|
||||
Get.snackbar(
|
||||
'Berhasil Disalin',
|
||||
'Kode registrasi telah disalin ke clipboard',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: AppColors.successLight,
|
||||
colorText: AppColors.success,
|
||||
margin: const EdgeInsets.all(16),
|
||||
);
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
splashRadius: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
'Simpan kode registrasi ini untuk memeriksa status pendaftaran Anda.',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -242,7 +330,7 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// Navigate back to login page
|
||||
Get.offAllNamed('/login');
|
||||
Get.offNamed(Routes.LOGIN);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_akun_bank_controller.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
|
||||
class PetugasAkunBankBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Register AsetProvider if not already registered
|
||||
if (!Get.isRegistered<AsetProvider>()) {
|
||||
Get.put(AsetProvider(), permanent: true);
|
||||
}
|
||||
|
||||
// Register PetugasAkunBankController
|
||||
Get.lazyPut<PetugasAkunBankController>(() => PetugasAkunBankController());
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_aset_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
|
||||
class PetugasAsetBinding extends Bindings {
|
||||
@override
|
||||
@ -10,6 +11,7 @@ class PetugasAsetBinding extends Bindings {
|
||||
Get.put(PetugasBumdesDashboardController(), permanent: true);
|
||||
}
|
||||
|
||||
Get.lazyPut<AsetProvider>(() => AsetProvider());
|
||||
Get.lazyPut<PetugasAsetController>(() => PetugasAsetController());
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_detail_penyewa_controller.dart';
|
||||
|
||||
class PetugasDetailPenyewaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<PetugasDetailPenyewaController>(
|
||||
() => PetugasDetailPenyewaController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,14 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_sewa_controller.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
|
||||
class PetugasDetailSewaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Ensure AsetProvider is registered
|
||||
if (!Get.isRegistered<AsetProvider>()) {
|
||||
Get.put(AsetProvider(), permanent: true);
|
||||
}
|
||||
// Memastikan controller sudah tersedia
|
||||
Get.lazyPut<PetugasSewaController>(
|
||||
() => PetugasSewaController(),
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_laporan_controller.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
|
||||
class PetugasLaporanBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Register AsetProvider if not already registered
|
||||
if (!Get.isRegistered<AsetProvider>()) {
|
||||
Get.put(AsetProvider(), permanent: true);
|
||||
}
|
||||
|
||||
// Register PetugasLaporanController
|
||||
Get.lazyPut<PetugasLaporanController>(() => PetugasLaporanController());
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,25 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||
import '../controllers/petugas_paket_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
|
||||
class PetugasPaketBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Register AsetProvider first
|
||||
if (!Get.isRegistered<AsetProvider>()) {
|
||||
Get.put(AsetProvider(), permanent: true);
|
||||
}
|
||||
|
||||
// Ensure dashboard controller is registered
|
||||
if (!Get.isRegistered<PetugasBumdesDashboardController>()) {
|
||||
Get.put(PetugasBumdesDashboardController(), permanent: true);
|
||||
}
|
||||
|
||||
Get.lazyPut<PetugasPaketController>(() => PetugasPaketController());
|
||||
// Register the controller
|
||||
Get.lazyPut<PetugasPaketController>(
|
||||
() => PetugasPaketController(),
|
||||
fenix: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_penyewa_controller.dart';
|
||||
|
||||
class PetugasPenyewaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.put<PetugasPenyewaController>(
|
||||
PetugasPenyewaController(),
|
||||
permanent: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,14 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_sewa_controller.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
|
||||
class PetugasSewaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Ensure AsetProvider is registered
|
||||
if (!Get.isRegistered<AsetProvider>()) {
|
||||
Get.put(AsetProvider(), permanent: true);
|
||||
}
|
||||
Get.lazyPut<PetugasSewaController>(() => PetugasSewaController());
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ class ListPetugasMitraController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Petugas mitra berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ class ListPetugasMitraController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Data petugas mitra berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -73,7 +73,7 @@ class ListPetugasMitraController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Petugas mitra berhasil dihapus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ class ListPetugasMitraController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Status Diperbarui',
|
||||
'Status petugas mitra diubah menjadi ${!currentStatus ? 'Aktif' : 'Nonaktif'}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
|
||||
class PetugasAkunBankController extends GetxController {
|
||||
final AsetProvider asetProvider = Get.find<AsetProvider>();
|
||||
|
||||
// Observable variables
|
||||
final isLoading = true.obs;
|
||||
final bankAccounts = <Map<String, dynamic>>[].obs;
|
||||
final errorMessage = ''.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadBankAccounts();
|
||||
}
|
||||
|
||||
// Load bank accounts from the database
|
||||
Future<void> loadBankAccounts() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
debugPrint('🏦 Loading bank accounts...');
|
||||
|
||||
// Fetch all bank accounts from the database
|
||||
final response = await asetProvider.client
|
||||
.from('akun_bank')
|
||||
.select('id, nama_bank, nama_akun, no_rekening')
|
||||
.order('nama_bank', ascending: true);
|
||||
|
||||
if (response is List) {
|
||||
bankAccounts.value = List<Map<String, dynamic>>.from(response);
|
||||
debugPrint('✅ Loaded ${bankAccounts.length} bank accounts');
|
||||
} else {
|
||||
bankAccounts.value = [];
|
||||
errorMessage.value = 'Failed to load bank accounts';
|
||||
debugPrint('❌ Failed to load bank accounts: Invalid response format');
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Error loading bank accounts: $e';
|
||||
debugPrint('❌ Error loading bank accounts: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new bank account
|
||||
Future<bool> addBankAccount(Map<String, dynamic> accountData) async {
|
||||
try {
|
||||
debugPrint('🏦 Adding new bank account: ${accountData['nama_bank']}');
|
||||
|
||||
final response = await asetProvider.client
|
||||
.from('akun_bank')
|
||||
.insert(accountData)
|
||||
.select('id');
|
||||
|
||||
if (response is List && response.isNotEmpty) {
|
||||
debugPrint('✅ Bank account added successfully');
|
||||
await loadBankAccounts(); // Reload the list
|
||||
return true;
|
||||
} else {
|
||||
debugPrint('❌ Failed to add bank account: No response');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error adding bank account: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update an existing bank account
|
||||
Future<bool> updateBankAccount(
|
||||
String id,
|
||||
Map<String, dynamic> accountData,
|
||||
) async {
|
||||
try {
|
||||
debugPrint('🏦 Updating bank account ID: $id');
|
||||
|
||||
final response = await asetProvider.client
|
||||
.from('akun_bank')
|
||||
.update(accountData)
|
||||
.eq('id', id);
|
||||
|
||||
debugPrint('✅ Bank account updated successfully');
|
||||
await loadBankAccounts(); // Reload the list
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error updating bank account: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete a bank account
|
||||
Future<bool> deleteBankAccount(String id) async {
|
||||
try {
|
||||
debugPrint('🏦 Deleting bank account ID: $id');
|
||||
|
||||
final response = await asetProvider.client
|
||||
.from('akun_bank')
|
||||
.delete()
|
||||
.eq('id', id);
|
||||
|
||||
debugPrint('✅ Bank account deleted successfully');
|
||||
await loadBankAccounts(); // Reload the list
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error deleting bank account: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,11 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
import '../../../data/models/aset_model.dart';
|
||||
|
||||
class PetugasAsetController extends GetxController {
|
||||
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
||||
// Observable lists for asset data
|
||||
final asetList = <Map<String, dynamic>>[].obs;
|
||||
final filteredAsetList = <Map<String, dynamic>>[].obs;
|
||||
@ -27,95 +32,100 @@ class PetugasAsetController extends GetxController {
|
||||
loadAsetData();
|
||||
}
|
||||
|
||||
// Load sample asset data (would be replaced with API call in production)
|
||||
// Load asset data from AsetProvider
|
||||
Future<void> loadAsetData() async {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// Simulate API call with a delay
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
isLoading.value = true;
|
||||
debugPrint('PetugasAsetController: Starting to load asset data...');
|
||||
|
||||
// Sample assets data
|
||||
final sampleData = [
|
||||
{
|
||||
'id': '1',
|
||||
'nama': 'Meja Rapat',
|
||||
'kategori': 'Furniture',
|
||||
'jenis': 'Sewa', // Added jenis field
|
||||
'harga': 50000,
|
||||
'satuan': 'per hari',
|
||||
'stok': 10,
|
||||
'deskripsi':
|
||||
'Meja rapat kayu jati ukuran besar untuk acara pertemuan',
|
||||
'gambar': 'https://example.com/meja.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
{
|
||||
'id': '2',
|
||||
'nama': 'Kursi Taman',
|
||||
'kategori': 'Furniture',
|
||||
'jenis': 'Sewa', // Added jenis field
|
||||
'harga': 10000,
|
||||
'satuan': 'per hari',
|
||||
'stok': 50,
|
||||
'deskripsi': 'Kursi taman plastik yang nyaman untuk acara outdoor',
|
||||
'gambar': 'https://example.com/kursi.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
{
|
||||
'id': '3',
|
||||
'nama': 'Proyektor',
|
||||
'kategori': 'Elektronik',
|
||||
'jenis': 'Sewa', // Added jenis field
|
||||
'harga': 100000,
|
||||
'satuan': 'per hari',
|
||||
'stok': 5,
|
||||
'deskripsi': 'Proyektor HD dengan brightness tinggi',
|
||||
'gambar': 'https://example.com/proyektor.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
{
|
||||
'id': '4',
|
||||
'nama': 'Sound System',
|
||||
'kategori': 'Elektronik',
|
||||
'jenis': 'Langganan', // Added jenis field
|
||||
'harga': 200000,
|
||||
'satuan': 'per bulan',
|
||||
'stok': 3,
|
||||
'deskripsi': 'Sound system lengkap dengan speaker dan mixer',
|
||||
'gambar': 'https://example.com/sound.jpg',
|
||||
'tersedia': false,
|
||||
},
|
||||
{
|
||||
'id': '5',
|
||||
'nama': 'Mobil Pick Up',
|
||||
'kategori': 'Kendaraan',
|
||||
'jenis': 'Langganan', // Added jenis field
|
||||
'harga': 250000,
|
||||
'satuan': 'per bulan',
|
||||
'stok': 2,
|
||||
'deskripsi': 'Mobil pick up untuk mengangkut barang',
|
||||
'gambar': 'https://example.com/pickup.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
{
|
||||
'id': '6',
|
||||
'nama': 'Internet Fiber',
|
||||
'kategori': 'Elektronik',
|
||||
'jenis': 'Langganan', // Added jenis field
|
||||
'harga': 350000,
|
||||
'satuan': 'per bulan',
|
||||
'stok': 15,
|
||||
'deskripsi': 'Paket internet fiber 100Mbps untuk kantor',
|
||||
'gambar': 'https://example.com/internet.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
];
|
||||
// Fetch data using AsetProvider
|
||||
final asetData = await _asetProvider.getSewaAsets();
|
||||
debugPrint(
|
||||
'PetugasAsetController: Fetched ${asetData.length} assets from Supabase',
|
||||
);
|
||||
|
||||
asetList.assignAll(sampleData);
|
||||
applyFilters(); // Apply default filters
|
||||
} catch (e) {
|
||||
print('Error loading asset data: $e');
|
||||
if (asetData.isEmpty) {
|
||||
debugPrint('PetugasAsetController: No assets found in Supabase');
|
||||
}
|
||||
|
||||
final List<Map<String, dynamic>> mappedAsets = [];
|
||||
int index = 0; // Initialize index counter
|
||||
for (var aset in asetData) {
|
||||
String displayKategori = 'Umum'; // Placeholder for descriptive category
|
||||
// Attempt to derive a more specific category from description if needed, or add to AsetModel
|
||||
if (aset.deskripsi.toLowerCase().contains('meja') ||
|
||||
aset.deskripsi.toLowerCase().contains('kursi')) {
|
||||
displayKategori = 'Furniture';
|
||||
} else if (aset.deskripsi.toLowerCase().contains('proyektor') ||
|
||||
aset.deskripsi.toLowerCase().contains('sound') ||
|
||||
aset.deskripsi.toLowerCase().contains('internet')) {
|
||||
displayKategori = 'Elektronik';
|
||||
} else if (aset.deskripsi.toLowerCase().contains('mobil') ||
|
||||
aset.deskripsi.toLowerCase().contains('kendaraan')) {
|
||||
displayKategori = 'Kendaraan';
|
||||
}
|
||||
|
||||
final map = {
|
||||
'id': aset.id,
|
||||
'nama': aset.nama,
|
||||
'deskripsi': aset.deskripsi,
|
||||
'harga':
|
||||
aset.satuanWaktuSewa.isNotEmpty
|
||||
? aset.satuanWaktuSewa.first['harga']
|
||||
: 0,
|
||||
'status': aset.status,
|
||||
'kategori': displayKategori,
|
||||
'jenis': aset.jenis ?? 'Sewa', // Add this line with default value
|
||||
'imageUrl': aset.imageUrl ?? 'https://via.placeholder.com/150',
|
||||
'satuan_waktu':
|
||||
aset.satuanWaktuSewa.isNotEmpty
|
||||
? aset.satuanWaktuSewa.first['nama_satuan_waktu'] ?? 'Hari'
|
||||
: 'Hari',
|
||||
'satuanWaktuSewa': aset.satuanWaktuSewa.toList(),
|
||||
};
|
||||
|
||||
debugPrint('Mapped asset #$index: $map');
|
||||
mappedAsets.add(map);
|
||||
index++;
|
||||
debugPrint('Deskripsi: ${aset.deskripsi}');
|
||||
debugPrint('Kategori (from AsetModel): ${aset.kategori}');
|
||||
debugPrint('Status: ${aset.status}');
|
||||
debugPrint('Mapped Kategori for Petugas View: ${map['kategori']}');
|
||||
debugPrint('Mapped Jenis for Petugas View: ${map['jenis']}');
|
||||
debugPrint('--------------------------------');
|
||||
}
|
||||
|
||||
// Populate asetList with fetched data and apply filters
|
||||
debugPrint(
|
||||
'PetugasAsetController: Mapped ${mappedAsets.length} assets for display',
|
||||
);
|
||||
asetList.assignAll(mappedAsets); // Make data available to UI
|
||||
debugPrint(
|
||||
'PetugasAsetController: asetList now has ${asetList.length} items',
|
||||
);
|
||||
|
||||
applyFilters(); // Apply initial filters
|
||||
debugPrint(
|
||||
'PetugasAsetController: Applied filters. filteredAsetList has ${filteredAsetList.length} items',
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
'PetugasAsetController: Data loading complete. Asset list populated and filters applied.',
|
||||
);
|
||||
debugPrint(
|
||||
'PetugasAsetController: First asset name: ${mappedAsets.isNotEmpty ? mappedAsets[0]['nama'] : 'No assets'}',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('PetugasAsetController: Error loading asset data: $e');
|
||||
debugPrint('PetugasAsetController: StackTrace: $stackTrace');
|
||||
// Optionally, show a snackbar or error message to the user
|
||||
Get.snackbar(
|
||||
'Error Memuat Data',
|
||||
'Gagal mengambil data aset dari server. Silakan coba lagi nanti.',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
@ -170,8 +180,10 @@ class PetugasAsetController extends GetxController {
|
||||
}
|
||||
|
||||
// Change tab (Sewa or Langganan)
|
||||
void changeTab(int index) {
|
||||
Future<void> changeTab(int index) async {
|
||||
selectedTabIndex.value = index;
|
||||
// Reload data when changing tabs to ensure we have the correct data for the selected tab
|
||||
await loadAsetData();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
@ -210,8 +222,63 @@ class PetugasAsetController extends GetxController {
|
||||
}
|
||||
|
||||
// Delete an asset
|
||||
void deleteAset(String id) {
|
||||
asetList.removeWhere((aset) => aset['id'] == id);
|
||||
applyFilters();
|
||||
Future<bool> deleteAset(String id) async {
|
||||
try {
|
||||
debugPrint('🗑️ Starting deletion process for asset ID: $id');
|
||||
|
||||
// Show loading indicator
|
||||
Get.dialog(
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
|
||||
// Call the provider to delete the asset
|
||||
final success = await _asetProvider.deleteAset(id);
|
||||
|
||||
// Close the loading dialog
|
||||
Get.back();
|
||||
|
||||
if (success) {
|
||||
// Remove the asset from our local list
|
||||
asetList.removeWhere((aset) => aset['id'] == id);
|
||||
// Apply filters to update the UI
|
||||
applyFilters();
|
||||
|
||||
// Show success message
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Aset berhasil dihapus',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
// Show error message
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan saat menghapus aset',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
// Close the loading dialog if still open
|
||||
if (Get.isDialogOpen ?? false) {
|
||||
Get.back();
|
||||
}
|
||||
|
||||
// Show error message
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal menghapus aset: ${e.toString()}',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ class PetugasBumdesCbpController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memuat data. Silakan coba lagi.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
@ -92,7 +92,7 @@ class PetugasBumdesCbpController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Rekening Utama',
|
||||
'Rekening ${account['bank_name']} telah dijadikan rekening utama',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -109,7 +109,7 @@ class PetugasBumdesCbpController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Rekening Ditambahkan',
|
||||
'Rekening bank baru telah berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
|
||||
@ -125,7 +125,7 @@ class PetugasBumdesCbpController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Rekening Diperbarui',
|
||||
'Informasi rekening bank telah berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -138,7 +138,7 @@ class PetugasBumdesCbpController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Tidak Dapat Menghapus',
|
||||
'Rekening utama tidak dapat dihapus. Silakan atur rekening lain sebagai utama terlebih dahulu.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -148,7 +148,7 @@ class PetugasBumdesCbpController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Rekening Dihapus',
|
||||
'Rekening bank telah berhasil dihapus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -164,7 +164,7 @@ class PetugasBumdesCbpController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Status Diperbarui',
|
||||
'Status mitra telah diubah menjadi ${partner['is_active'] ? 'Aktif' : 'Tidak Aktif'}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -181,7 +181,7 @@ class PetugasBumdesCbpController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Mitra Ditambahkan',
|
||||
'Mitra baru telah berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
|
||||
@ -197,7 +197,7 @@ class PetugasBumdesCbpController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Mitra Diperbarui',
|
||||
'Informasi mitra telah berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -210,7 +210,7 @@ class PetugasBumdesCbpController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Mitra Dihapus',
|
||||
'Mitra telah berhasil dihapus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:bumrent_app/app/data/providers/auth_provider.dart';
|
||||
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||
import 'package:bumrent_app/app/data/providers/pesanan_provider.dart';
|
||||
import 'package:bumrent_app/app/routes/app_routes.dart';
|
||||
import '../../../services/sewa_service.dart';
|
||||
import '../../../services/service_manager.dart';
|
||||
import '../../../data/models/pembayaran_model.dart';
|
||||
import '../../../services/pembayaran_service.dart';
|
||||
|
||||
class PetugasBumdesDashboardController extends GetxController {
|
||||
AuthProvider? _authProvider;
|
||||
@ -8,40 +16,61 @@ class PetugasBumdesDashboardController extends GetxController {
|
||||
// Reactive variables
|
||||
final userEmail = ''.obs;
|
||||
final currentTabIndex = 0.obs;
|
||||
final avatarUrl = ''.obs;
|
||||
final userName = ''.obs;
|
||||
|
||||
// Revenue Statistics
|
||||
final totalPendapatanBulanIni = 'Rp 8.500.000'.obs;
|
||||
final totalPendapatanBulanLalu = 'Rp 7.200.000'.obs;
|
||||
final persentaseKenaikan = '18%'.obs;
|
||||
final totalPendapatanBulanIni = ''.obs;
|
||||
final totalPendapatanBulanLalu = ''.obs;
|
||||
final persentaseKenaikan = ''.obs;
|
||||
final isKenaikanPositif = true.obs;
|
||||
|
||||
// Revenue by Category
|
||||
final pendapatanSewa = 'Rp 5.200.000'.obs;
|
||||
final persentaseSewa = 100.obs;
|
||||
final pendapatanSewa = ''.obs;
|
||||
final persentaseSewa = 0.obs;
|
||||
|
||||
// Revenue Trends (last 6 months)
|
||||
final trendPendapatan = [4.2, 5.1, 4.8, 6.2, 7.2, 8.5].obs; // in millions
|
||||
final trendPendapatan = <double>[].obs; // 6 bulan terakhir
|
||||
|
||||
// Status Counters for Sewa Aset
|
||||
final terlaksanaCount = 5.obs;
|
||||
final dijadwalkanCount = 1.obs;
|
||||
final aktifCount = 1.obs;
|
||||
final dibatalkanCount = 3.obs;
|
||||
final terlaksanaCount = 0.obs;
|
||||
final dijadwalkanCount = 0.obs;
|
||||
final aktifCount = 0.obs;
|
||||
final dibatalkanCount = 0.obs;
|
||||
|
||||
// Additional Sewa Aset Status Counters
|
||||
final menungguPembayaranCount = 2.obs;
|
||||
final periksaPembayaranCount = 1.obs;
|
||||
final diterimaCount = 3.obs;
|
||||
final pembayaranDendaCount = 1.obs;
|
||||
final menungguPembayaranCount = 0.obs;
|
||||
final periksaPembayaranCount = 0.obs;
|
||||
final diterimaCount = 0.obs;
|
||||
final pembayaranDendaCount = 0.obs;
|
||||
final periksaPembayaranDendaCount = 0.obs;
|
||||
final selesaiCount = 4.obs;
|
||||
final selesaiCount = 0.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;
|
||||
final pengajuanSewaCount = 0.obs;
|
||||
final pemasanganCountSewa = 0.obs;
|
||||
final sewaAktifCount = 0.obs;
|
||||
final tagihanAktifCountSewa = 0.obs;
|
||||
final periksaPembayaranCountSewa = 0.obs;
|
||||
|
||||
// Tenant (Penyewa) Statistics
|
||||
final penyewaPendingCount = 0.obs;
|
||||
final penyewaActiveCount = 0.obs;
|
||||
final penyewaSuspendedCount = 0.obs;
|
||||
final penyewaTotalCount = 0.obs;
|
||||
final isPenyewaStatsLoading = true.obs;
|
||||
|
||||
// Statistik pendapatan
|
||||
final totalPendapatan = 0.obs;
|
||||
final pendapatanBulanIni = 0.obs;
|
||||
final pendapatanBulanLalu = 0.obs;
|
||||
final pendapatanTunai = 0.obs;
|
||||
final pendapatanTransfer = 0.obs;
|
||||
final trenPendapatan = <int>[].obs; // 6 bulan terakhir
|
||||
|
||||
// Dashboard statistics
|
||||
final pembayaranStats = <String, dynamic>{}.obs;
|
||||
final isStatsLoading = true.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -49,36 +78,160 @@ class PetugasBumdesDashboardController extends GetxController {
|
||||
try {
|
||||
_authProvider = Get.find<AuthProvider>();
|
||||
userEmail.value = _authProvider?.currentUser?.email ?? 'Tidak ada email';
|
||||
fetchPetugasAvatar();
|
||||
fetchPetugasName();
|
||||
} catch (e) {
|
||||
print('Error finding AuthProvider: $e');
|
||||
userEmail.value = 'Tidak ada email';
|
||||
}
|
||||
|
||||
// In a real app, these counts would be fetched from backend
|
||||
// loadStatusCounts();
|
||||
print('✅ PetugasBumdesDashboardController initialized successfully');
|
||||
print('\u2705 PetugasBumdesDashboardController initialized successfully');
|
||||
countSewaByStatus();
|
||||
fetchPembayaranStats();
|
||||
fetchPenyewaStats();
|
||||
}
|
||||
|
||||
// Method to load status counts from backend
|
||||
// Future<void> loadStatusCounts() async {
|
||||
// try {
|
||||
// final response = await _asetProvider.getSewaStatusCounts();
|
||||
// if (response != null) {
|
||||
// terlaksanaCount.value = response['terlaksana'] ?? 0;
|
||||
// dijadwalkanCount.value = response['dijadwalkan'] ?? 0;
|
||||
// aktifCount.value = response['aktif'] ?? 0;
|
||||
// dibatalkanCount.value = response['dibatalkan'] ?? 0;
|
||||
// menungguPembayaranCount.value = response['menunggu_pembayaran'] ?? 0;
|
||||
// periksaPembayaranCount.value = response['periksa_pembayaran'] ?? 0;
|
||||
// diterimaCount.value = response['diterima'] ?? 0;
|
||||
// pembayaranDendaCount.value = response['pembayaran_denda'] ?? 0;
|
||||
// periksaPembayaranDendaCount.value = response['periksa_pembayaran_denda'] ?? 0;
|
||||
// selesaiCount.value = response['selesai'] ?? 0;
|
||||
// }
|
||||
// } catch (e) {
|
||||
// print('Error loading status counts: $e');
|
||||
// }
|
||||
// }
|
||||
Future<void> countSewaByStatus() async {
|
||||
try {
|
||||
final data = await SewaService().fetchAllSewa();
|
||||
menungguPembayaranCount.value =
|
||||
data.where((s) => s.status == 'MENUNGGU PEMBAYARAN').length;
|
||||
periksaPembayaranCount.value =
|
||||
data.where((s) => s.status == 'PERIKSA PEMBAYARAN').length;
|
||||
diterimaCount.value = data.where((s) => s.status == 'DITERIMA').length;
|
||||
pembayaranDendaCount.value =
|
||||
data.where((s) => s.status == 'PEMBAYARAN DENDA').length;
|
||||
periksaPembayaranDendaCount.value =
|
||||
data.where((s) => s.status == 'PERIKSA PEMBAYARAN DENDA').length;
|
||||
selesaiCount.value = data.where((s) => s.status == 'SELESAI').length;
|
||||
print(
|
||||
'Count for MENUNGGU PEMBAYARAN: \\${menungguPembayaranCount.value}',
|
||||
);
|
||||
print('Count for PERIKSA PEMBAYARAN: \\${periksaPembayaranCount.value}');
|
||||
print('Count for DITERIMA: \\${diterimaCount.value}');
|
||||
print('Count for PEMBAYARAN DENDA: \\${pembayaranDendaCount.value}');
|
||||
print(
|
||||
'Count for PERIKSA PEMBAYARAN DENDA: \\${periksaPembayaranDendaCount.value}',
|
||||
);
|
||||
print('Count for SELESAI: \\${selesaiCount.value}');
|
||||
} catch (e) {
|
||||
print('Error counting sewa by status: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchPembayaranStats() async {
|
||||
isStatsLoading.value = true;
|
||||
try {
|
||||
final stats = await PembayaranService().fetchStats();
|
||||
pembayaranStats.value = stats;
|
||||
// Set trendPendapatan from stats['trendPerMonth'] if available
|
||||
if (stats['trendPerMonth'] != null) {
|
||||
trendPendapatan.value = List<double>.from(stats['trendPerMonth']);
|
||||
}
|
||||
print('Pembayaran stats: $stats');
|
||||
} catch (e, st) {
|
||||
print('Error fetching pembayaran stats: $e\n$st');
|
||||
pembayaranStats.value = {};
|
||||
trendPendapatan.value = [];
|
||||
}
|
||||
isStatsLoading.value = false;
|
||||
}
|
||||
|
||||
Future<void> fetchPetugasAvatar() async {
|
||||
try {
|
||||
final userId = _authProvider?.getCurrentUserId();
|
||||
if (userId == null) return;
|
||||
final client = _authProvider!.client;
|
||||
final data =
|
||||
await client
|
||||
.from('petugas_bumdes')
|
||||
.select('avatar')
|
||||
.eq('id', userId)
|
||||
.maybeSingle();
|
||||
if (data != null &&
|
||||
data['avatar'] != null &&
|
||||
data['avatar'].toString().isNotEmpty) {
|
||||
avatarUrl.value = data['avatar'].toString();
|
||||
} else {
|
||||
avatarUrl.value = '';
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching petugas avatar: $e');
|
||||
avatarUrl.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchPetugasName() async {
|
||||
try {
|
||||
final userId = _authProvider?.getCurrentUserId();
|
||||
if (userId == null) return;
|
||||
final client = _authProvider!.client;
|
||||
final data =
|
||||
await client
|
||||
.from('petugas_bumdes')
|
||||
.select('nama')
|
||||
.eq('id', userId)
|
||||
.maybeSingle();
|
||||
if (data != null &&
|
||||
data['nama'] != null &&
|
||||
data['nama'].toString().isNotEmpty) {
|
||||
userName.value = data['nama'].toString();
|
||||
} else {
|
||||
userName.value = '';
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching petugas name: $e');
|
||||
userName.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchPenyewaStats() async {
|
||||
isPenyewaStatsLoading.value = true;
|
||||
try {
|
||||
if (_authProvider == null || _authProvider!.client == null) {
|
||||
print('Auth provider or client is null');
|
||||
return;
|
||||
}
|
||||
|
||||
final data = await _authProvider!.client
|
||||
.from('warga_desa')
|
||||
.select('status, user_id')
|
||||
.not('user_id', 'is', null);
|
||||
|
||||
if (data != null) {
|
||||
final List<dynamic> penyewaList = data as List<dynamic>;
|
||||
|
||||
// Count penyewa by status
|
||||
penyewaPendingCount.value =
|
||||
penyewaList
|
||||
.where(
|
||||
(p) => p['status']?.toString().toLowerCase() == 'pending',
|
||||
)
|
||||
.length;
|
||||
|
||||
penyewaActiveCount.value =
|
||||
penyewaList
|
||||
.where((p) => p['status']?.toString().toLowerCase() == 'active')
|
||||
.length;
|
||||
|
||||
penyewaSuspendedCount.value =
|
||||
penyewaList
|
||||
.where(
|
||||
(p) => p['status']?.toString().toLowerCase() == 'suspended',
|
||||
)
|
||||
.length;
|
||||
|
||||
penyewaTotalCount.value = penyewaList.length;
|
||||
|
||||
print(
|
||||
'Penyewa stats - Pending: ${penyewaPendingCount.value}, Active: ${penyewaActiveCount.value}, Suspended: ${penyewaSuspendedCount.value}, Total: ${penyewaTotalCount.value}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching penyewa stats: $e');
|
||||
} finally {
|
||||
isPenyewaStatsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void changeTab(int index) {
|
||||
try {
|
||||
@ -102,6 +255,10 @@ class PetugasBumdesDashboardController extends GetxController {
|
||||
// Navigate to Sewa page
|
||||
navigateToSewa();
|
||||
break;
|
||||
case 4:
|
||||
// Navigate to Penyewa page
|
||||
navigateToPenyewa();
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error changing tab: $e');
|
||||
@ -132,16 +289,66 @@ class PetugasBumdesDashboardController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
void navigateToPenyewa() {
|
||||
try {
|
||||
Get.offAllNamed(Routes.PETUGAS_PENYEWA);
|
||||
} catch (e) {
|
||||
print('Error navigating to Penyewa: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void logout() async {
|
||||
try {
|
||||
// Store login route for navigation
|
||||
final loginRoute = Routes.LOGIN;
|
||||
|
||||
// Sign out from Supabase
|
||||
if (_authProvider != null) {
|
||||
await _authProvider!.signOut();
|
||||
}
|
||||
Get.offAllNamed(Routes.LOGIN);
|
||||
|
||||
// Navigate to login screen while context is still valid
|
||||
Get.offAllNamed(loginRoute);
|
||||
|
||||
// Clear auth provider data if available
|
||||
if (_authProvider != null) {
|
||||
_authProvider!.clearAuthData();
|
||||
}
|
||||
|
||||
// Clear provider caches
|
||||
_clearProviderCaches();
|
||||
|
||||
// Clean up GetX controllers but keep navigation intact
|
||||
Get.deleteAll(force: false);
|
||||
} catch (e) {
|
||||
print('Error during logout: $e');
|
||||
// Still try to navigate to login even if sign out fails
|
||||
Get.offAllNamed(Routes.LOGIN);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to clear provider caches that need explicit clearing
|
||||
void _clearProviderCaches() {
|
||||
try {
|
||||
// Clear AsetProvider cache
|
||||
if (Get.isRegistered<AsetProvider>()) {
|
||||
final asetProvider = Get.find<AsetProvider>();
|
||||
asetProvider.clearCache();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error clearing AsetProvider: $e');
|
||||
}
|
||||
|
||||
try {
|
||||
// Clear PesananProvider cache
|
||||
if (Get.isRegistered<PesananProvider>()) {
|
||||
final pesananProvider = Get.find<PesananProvider>();
|
||||
pesananProvider.clearCache();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error clearing PesananProvider: $e');
|
||||
}
|
||||
|
||||
// Add other providers here that need explicit cache clearing
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,232 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
|
||||
class PetugasDetailPenyewaController extends GetxController {
|
||||
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
||||
|
||||
final isLoading = true.obs;
|
||||
final penyewaDetail = Rx<Map<String, dynamic>>({});
|
||||
final sewaHistory = <Map<String, dynamic>>[].obs;
|
||||
final userId = ''.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
if (Get.arguments != null && Get.arguments['userId'] != null) {
|
||||
userId.value = Get.arguments['userId'];
|
||||
fetchPenyewaDetail();
|
||||
fetchSewaHistory();
|
||||
} else {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Data penyewa tidak ditemukan',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchPenyewaDetail() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
final data =
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.select('*')
|
||||
.eq('user_id', userId.value)
|
||||
.single();
|
||||
|
||||
if (data != null) {
|
||||
penyewaDetail.value = Map<String, dynamic>.from(data);
|
||||
print('Penyewa detail fetched: ${penyewaDetail.value}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching penyewa detail: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memuat data penyewa',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchSewaHistory() async {
|
||||
try {
|
||||
final data = await _authProvider.client
|
||||
.from('sewa_aset')
|
||||
.select('*')
|
||||
.eq('user_id', userId.value)
|
||||
.order('created_at', ascending: false);
|
||||
|
||||
if (data != null) {
|
||||
sewaHistory.value = List<Map<String, dynamic>>.from(data);
|
||||
print('Sewa history fetched: ${sewaHistory.length} items');
|
||||
|
||||
// Process data for each item
|
||||
for (int i = 0; i < sewaHistory.length; i++) {
|
||||
final item = sewaHistory[i];
|
||||
|
||||
// Fetch tagihan data for this sewa_aset
|
||||
try {
|
||||
final tagihanResponse =
|
||||
await _authProvider.client
|
||||
.from('tagihan_sewa')
|
||||
.select('tagihan_awal, denda, tagihan_dibayar')
|
||||
.eq('sewa_aset_id', item['id'])
|
||||
.maybeSingle();
|
||||
|
||||
if (tagihanResponse != null) {
|
||||
// Add tagihan data to the item
|
||||
sewaHistory[i] = {...item, 'tagihan_sewa': tagihanResponse};
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching tagihan for sewa_aset ${item['id']}: $e');
|
||||
}
|
||||
|
||||
// Get the updated item after adding tagihan data
|
||||
final updatedItem = sewaHistory[i];
|
||||
|
||||
// If this is a package rental (aset_id is null and paket_id exists)
|
||||
if (updatedItem['aset_id'] == null &&
|
||||
updatedItem['paket_id'] != null) {
|
||||
final String paketId = updatedItem['paket_id'];
|
||||
|
||||
try {
|
||||
// Get package name from paket table
|
||||
final paketResponse =
|
||||
await _authProvider.client
|
||||
.from('paket')
|
||||
.select('nama')
|
||||
.eq('id', paketId)
|
||||
.maybeSingle();
|
||||
|
||||
// Get package photo from foto_aset table
|
||||
final fotoResponse =
|
||||
await _authProvider.client
|
||||
.from('foto_aset')
|
||||
.select('foto_aset')
|
||||
.eq('id_paket', paketId)
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
// Create a synthetic aset object for paket
|
||||
Map<String, dynamic> syntheticAset = {};
|
||||
|
||||
if (paketResponse != null) {
|
||||
syntheticAset['nama'] = paketResponse['nama'] ?? 'Paket';
|
||||
}
|
||||
|
||||
if (fotoResponse != null) {
|
||||
syntheticAset['foto_utama'] = fotoResponse['foto_aset'];
|
||||
}
|
||||
|
||||
// Update the item with the synthetic aset
|
||||
sewaHistory[i] = {
|
||||
...updatedItem,
|
||||
'aset': syntheticAset,
|
||||
'tipe_pesanan': 'paket',
|
||||
};
|
||||
} catch (e) {
|
||||
print('Error fetching package details: $e');
|
||||
}
|
||||
}
|
||||
// If this is an asset rental (aset_id exists and paket_id is null)
|
||||
else if (updatedItem['aset_id'] != null &&
|
||||
updatedItem['paket_id'] == null) {
|
||||
final String asetId = updatedItem['aset_id'];
|
||||
|
||||
try {
|
||||
// Get asset name from aset table
|
||||
final asetResponse =
|
||||
await _authProvider.client
|
||||
.from('aset')
|
||||
.select('nama')
|
||||
.eq('id', asetId)
|
||||
.maybeSingle();
|
||||
|
||||
// Get asset photo from foto_aset table using id_aset
|
||||
final fotoResponse =
|
||||
await _authProvider.client
|
||||
.from('foto_aset')
|
||||
.select('foto_aset')
|
||||
.eq('id_aset', asetId)
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
// Create aset object for individual asset
|
||||
Map<String, dynamic> asetData = {};
|
||||
|
||||
if (asetResponse != null) {
|
||||
asetData['nama'] = asetResponse['nama'] ?? 'Aset';
|
||||
}
|
||||
|
||||
if (fotoResponse != null) {
|
||||
asetData['foto_utama'] = fotoResponse['foto_aset'];
|
||||
}
|
||||
|
||||
// Update the item with the aset data
|
||||
sewaHistory[i] = {
|
||||
...updatedItem,
|
||||
'aset': asetData,
|
||||
'tipe_pesanan': 'aset',
|
||||
};
|
||||
} catch (e) {
|
||||
print('Error fetching asset details: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching sewa history: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updatePenyewaStatus(String status, String keterangan) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.update({
|
||||
'status': status,
|
||||
'keterangan': keterangan,
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
})
|
||||
.eq('user_id', userId.value);
|
||||
|
||||
// Refresh data
|
||||
await fetchPenyewaDetail();
|
||||
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Status penyewa berhasil diperbarui',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error updating penyewa status: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memperbarui status penyewa',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
String getStatusLabel(String? status) {
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'active':
|
||||
return 'Aktif';
|
||||
case 'pending':
|
||||
return 'Menunggu Verifikasi';
|
||||
case 'suspended':
|
||||
return 'Dinonaktifkan';
|
||||
default:
|
||||
return 'Tidak diketahui';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -79,7 +79,7 @@ class PetugasManajemenBumdesController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Rekening utama berhasil diubah',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
|
||||
@ -93,7 +93,7 @@ class PetugasManajemenBumdesController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Status mitra berhasil diubah',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
|
||||
@ -110,7 +110,7 @@ class PetugasManajemenBumdesController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Rekening bank berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
|
||||
@ -124,7 +124,7 @@ class PetugasManajemenBumdesController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Rekening bank berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
|
||||
@ -145,7 +145,7 @@ class PetugasManajemenBumdesController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Rekening bank berhasil dihapus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
|
||||
@ -155,7 +155,7 @@ class PetugasManajemenBumdesController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Mitra berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
|
||||
@ -166,7 +166,7 @@ class PetugasManajemenBumdesController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Mitra berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
|
||||
@ -177,7 +177,7 @@ class PetugasManajemenBumdesController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Mitra berhasil dihapus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/intl.dart' show NumberFormat;
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:bumrent_app/app/data/models/paket_model.dart';
|
||||
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||
|
||||
class PetugasPaketController extends GetxController {
|
||||
final isLoading = false.obs;
|
||||
final searchQuery = ''.obs;
|
||||
final selectedCategory = 'Semua'.obs;
|
||||
final sortBy = 'Terbaru'.obs;
|
||||
// Dependencies
|
||||
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
||||
|
||||
// Kategori untuk filter
|
||||
final categories = <String>[
|
||||
'Semua',
|
||||
'Pesta',
|
||||
'Rapat',
|
||||
'Olahraga',
|
||||
'Pernikahan',
|
||||
'Lainnya',
|
||||
];
|
||||
// State
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxString searchQuery = ''.obs;
|
||||
final RxString selectedCategory = 'Semua'.obs;
|
||||
final RxString sortBy = 'Terbaru'.obs;
|
||||
final RxList<PaketModel> packages = <PaketModel>[].obs;
|
||||
final RxList<PaketModel> filteredPackages = <PaketModel>[].obs;
|
||||
|
||||
// Opsi pengurutan
|
||||
final sortOptions = <String>[
|
||||
// Sort options for the dropdown
|
||||
final List<String> sortOptions = [
|
||||
'Terbaru',
|
||||
'Terlama',
|
||||
'Harga Tertinggi',
|
||||
@ -27,171 +27,256 @@ class PetugasPaketController extends GetxController {
|
||||
'Nama Z-A',
|
||||
];
|
||||
|
||||
// Data dummy paket
|
||||
final paketList = <Map<String, dynamic>>[].obs;
|
||||
final filteredPaketList = <Map<String, dynamic>>[].obs;
|
||||
// For backward compatibility
|
||||
final RxList<Map<String, dynamic>> paketList = <Map<String, dynamic>>[].obs;
|
||||
final RxList<Map<String, dynamic>> filteredPaketList =
|
||||
<Map<String, dynamic>>[].obs;
|
||||
|
||||
// Logger
|
||||
late final Logger _logger;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadPaketData();
|
||||
}
|
||||
|
||||
// Format harga ke Rupiah
|
||||
String formatPrice(int price) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
// Initialize logger
|
||||
_logger = Logger(
|
||||
printer: PrettyPrinter(
|
||||
methodCount: 0,
|
||||
errorMethodCount: 5,
|
||||
colors: true,
|
||||
printEmojis: true,
|
||||
),
|
||||
);
|
||||
return formatter.format(price);
|
||||
|
||||
// Load initial data
|
||||
fetchPackages();
|
||||
}
|
||||
|
||||
// Load data paket dummy
|
||||
/// Fetch packages from the API
|
||||
Future<void> fetchPackages() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
_logger.i('🔄 [fetchPackages] Fetching packages...');
|
||||
|
||||
final result = await _asetProvider.getAllPaket();
|
||||
|
||||
if (result.isEmpty) {
|
||||
_logger.w('ℹ️ [fetchPackages] No packages found');
|
||||
packages.clear();
|
||||
filteredPackages.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
packages.assignAll(result);
|
||||
filteredPackages.assignAll(result);
|
||||
|
||||
// Update legacy list for backward compatibility
|
||||
_updateLegacyPaketList();
|
||||
|
||||
_logger.i(
|
||||
'✅ [fetchPackages] Successfully loaded ${result.length} packages',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e(
|
||||
'❌ [fetchPackages] Error fetching packages',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memuat data paket. Silakan coba lagi.',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update legacy paketList for backward compatibility
|
||||
void _updateLegacyPaketList() {
|
||||
try {
|
||||
_logger.d('🔄 [_updateLegacyPaketList] Updating legacy paketList...');
|
||||
|
||||
final List<Map<String, dynamic>> legacyList =
|
||||
packages.map((pkg) {
|
||||
return {
|
||||
'id': pkg.id,
|
||||
'nama': pkg.nama,
|
||||
'deskripsi': pkg.deskripsi,
|
||||
'harga': pkg.harga,
|
||||
'kuantitas': pkg.kuantitas,
|
||||
'status': pkg.status, // Add status to legacy mapping
|
||||
'foto': pkg.foto,
|
||||
'foto_paket': pkg.foto_paket,
|
||||
'images': pkg.images,
|
||||
'satuanWaktuSewa': pkg.satuanWaktuSewa,
|
||||
'created_at': pkg.createdAt,
|
||||
'updated_at': pkg.updatedAt,
|
||||
};
|
||||
}).toList();
|
||||
|
||||
paketList.assignAll(legacyList);
|
||||
filteredPaketList.assignAll(legacyList);
|
||||
|
||||
_logger.d(
|
||||
'✅ [_updateLegacyPaketList] Updated ${legacyList.length} packages',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e(
|
||||
'❌ [_updateLegacyPaketList] Error updating legacy list',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// For backward compatibility
|
||||
Future<void> loadPaketData() async {
|
||||
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;
|
||||
_logger.d('ℹ️ [loadPaketData] Using fetchPackages() instead');
|
||||
await fetchPackages();
|
||||
}
|
||||
|
||||
// Filter paket berdasarkan search query dan kategori
|
||||
/// Filter packages based on search query and category
|
||||
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(),
|
||||
);
|
||||
try {
|
||||
_logger.d('🔄 [filterPaket] Filtering packages...');
|
||||
|
||||
final matchesCategory =
|
||||
selectedCategory.value == 'Semua' ||
|
||||
paket['kategori'] == selectedCategory.value;
|
||||
if (searchQuery.value.isEmpty && selectedCategory.value == 'Semua') {
|
||||
filteredPackages.value = List.from(packages);
|
||||
filteredPaketList.value = List.from(paketList);
|
||||
} else {
|
||||
// Filter new packages
|
||||
filteredPackages.value =
|
||||
packages.where((paket) {
|
||||
final matchesSearch =
|
||||
searchQuery.value.isEmpty ||
|
||||
paket.nama.toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
);
|
||||
|
||||
return matchesQuery && matchesCategory;
|
||||
}).toList();
|
||||
// For now, we're not using categories in the new model
|
||||
// You can add category filtering if needed
|
||||
final matchesCategory = selectedCategory.value == 'Semua';
|
||||
|
||||
// Sort the filtered list
|
||||
sortFilteredList();
|
||||
return matchesSearch && matchesCategory;
|
||||
}).toList();
|
||||
|
||||
// Also update legacy list for backward compatibility
|
||||
filteredPaketList.value =
|
||||
paketList.where((paket) {
|
||||
final matchesSearch =
|
||||
searchQuery.value.isEmpty ||
|
||||
(paket['nama']?.toString() ?? '').toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
);
|
||||
|
||||
// For legacy support, check if category exists
|
||||
final matchesCategory =
|
||||
selectedCategory.value == 'Semua' ||
|
||||
(paket['kategori']?.toString() ?? '') ==
|
||||
selectedCategory.value;
|
||||
|
||||
return matchesSearch && matchesCategory;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
sortFilteredList();
|
||||
_logger.d(
|
||||
'✅ [filterPaket] Filtered to ${filteredPackages.length} packages',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e(
|
||||
'❌ [filterPaket] Error filtering packages',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the filtered list
|
||||
/// Sort the filtered list based on the selected sort option
|
||||
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;
|
||||
try {
|
||||
_logger.d('🔄 [sortFilteredList] Sorting packages by ${sortBy.value}');
|
||||
|
||||
// Sort new packages
|
||||
switch (sortBy.value) {
|
||||
case 'Terbaru':
|
||||
filteredPackages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
break;
|
||||
case 'Terlama':
|
||||
filteredPackages.sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||
break;
|
||||
case 'Harga Tertinggi':
|
||||
filteredPackages.sort((a, b) => b.harga.compareTo(a.harga));
|
||||
break;
|
||||
case 'Harga Terendah':
|
||||
filteredPackages.sort((a, b) => a.harga.compareTo(b.harga));
|
||||
break;
|
||||
case 'Nama A-Z':
|
||||
filteredPackages.sort((a, b) => a.nama.compareTo(b.nama));
|
||||
break;
|
||||
case 'Nama Z-A':
|
||||
filteredPackages.sort((a, b) => b.nama.compareTo(a.nama));
|
||||
break;
|
||||
}
|
||||
|
||||
// Also sort legacy list for backward compatibility
|
||||
switch (sortBy.value) {
|
||||
case 'Terbaru':
|
||||
filteredPaketList.sort(
|
||||
(a, b) => ((b['created_at'] ?? '') as String).compareTo(
|
||||
(a['created_at'] ?? '') as String,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'Terlama':
|
||||
filteredPaketList.sort(
|
||||
(a, b) => ((a['created_at'] ?? '') as String).compareTo(
|
||||
(b['created_at'] ?? '') as String,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'Harga Tertinggi':
|
||||
filteredPaketList.sort(
|
||||
(a, b) =>
|
||||
((b['harga'] ?? 0) as int).compareTo((a['harga'] ?? 0) as int),
|
||||
);
|
||||
break;
|
||||
case 'Harga Terendah':
|
||||
filteredPaketList.sort(
|
||||
(a, b) =>
|
||||
((a['harga'] ?? 0) as int).compareTo((b['harga'] ?? 0) as int),
|
||||
);
|
||||
break;
|
||||
case 'Nama A-Z':
|
||||
filteredPaketList.sort(
|
||||
(a, b) => ((a['nama'] ?? '') as String).compareTo(
|
||||
(b['nama'] ?? '') as String,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'Nama Z-A':
|
||||
filteredPaketList.sort(
|
||||
(a, b) => ((b['nama'] ?? '') as String).compareTo(
|
||||
(a['nama'] ?? '') as String,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.d(
|
||||
'✅ [sortFilteredList] Sorted ${filteredPackages.length} packages',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e(
|
||||
'❌ [sortFilteredList] Error sorting packages',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,40 +299,179 @@ class PetugasPaketController extends GetxController {
|
||||
}
|
||||
|
||||
// Tambah paket baru
|
||||
void addPaket(Map<String, dynamic> paket) {
|
||||
paketList.add(paket);
|
||||
filterPaket();
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Paket baru berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
Future<void> addPaket(Map<String, dynamic> paketData) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// Edit paket
|
||||
void editPaket(String id, Map<String, dynamic> updatedPaket) {
|
||||
final index = paketList.indexWhere((element) => element['id'] == id);
|
||||
if (index >= 0) {
|
||||
paketList[index] = updatedPaket;
|
||||
// Convert to PaketModel
|
||||
final newPaket = PaketModel.fromJson({
|
||||
...paketData,
|
||||
'id': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
});
|
||||
|
||||
// Add to the list
|
||||
packages.add(newPaket);
|
||||
_updateLegacyPaketList();
|
||||
filterPaket();
|
||||
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Paket berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
'Paket baru berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e(
|
||||
'❌ [addPaket] Error adding package',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal menambahkan paket. Silakan coba lagi.',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Edit paket
|
||||
Future<void> editPaket(String id, Map<String, dynamic> updatedData) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
final index = packages.indexWhere((pkg) => pkg.id == id);
|
||||
if (index >= 0) {
|
||||
// Update the package
|
||||
final updatedPaket = packages[index].copyWith(
|
||||
nama: updatedData['nama']?.toString() ?? packages[index].nama,
|
||||
deskripsi:
|
||||
updatedData['deskripsi']?.toString() ?? packages[index].deskripsi,
|
||||
kuantitas:
|
||||
(updatedData['kuantitas'] is int)
|
||||
? updatedData['kuantitas']
|
||||
: (int.tryParse(
|
||||
updatedData['kuantitas']?.toString() ?? '0',
|
||||
) ??
|
||||
packages[index].kuantitas),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
packages[index] = updatedPaket;
|
||||
_updateLegacyPaketList();
|
||||
filterPaket();
|
||||
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Paket berhasil diperbarui',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e(
|
||||
'❌ [editPaket] Error updating package',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memperbarui paket. Silakan coba lagi.',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Hapus paket
|
||||
void deletePaket(String id) {
|
||||
paketList.removeWhere((element) => element['id'] == id);
|
||||
filterPaket();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Paket berhasil dihapus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
Future<void> deletePaket(String id) async {
|
||||
try {
|
||||
_logger.i(
|
||||
'🔄 [deletePaket] Starting deletion process for package ID: $id',
|
||||
);
|
||||
|
||||
// Show a loading dialog
|
||||
Get.dialog(
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
|
||||
// Call the provider to delete the package and all related data from Supabase
|
||||
final success = await _asetProvider.deletePaket(id);
|
||||
|
||||
// Close the loading dialog
|
||||
Get.back();
|
||||
|
||||
if (success) {
|
||||
_logger.i('✅ [deletePaket] Package deleted successfully from database');
|
||||
|
||||
// Remove the package from the UI lists
|
||||
packages.removeWhere((pkg) => pkg.id == id);
|
||||
_updateLegacyPaketList();
|
||||
filterPaket();
|
||||
|
||||
// Show success message
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Paket berhasil dihapus dari sistem',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
} else {
|
||||
_logger.e('❌ [deletePaket] Failed to delete package from database');
|
||||
|
||||
// Show error message
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan saat menghapus paket',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e(
|
||||
'❌ [deletePaket] Error deleting package',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
// Close the loading dialog if still open
|
||||
if (Get.isDialogOpen ?? false) {
|
||||
Get.back();
|
||||
}
|
||||
|
||||
// Show error message
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal menghapus paket: ${e.toString()}',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Format price to Rupiah currency
|
||||
String formatPrice(num price) {
|
||||
return 'Rp ${NumberFormat('#,##0', 'id_ID').format(price)}';
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,230 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
|
||||
class PetugasPenyewaController extends GetxController {
|
||||
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
||||
|
||||
// Reactive variables
|
||||
final isLoading = true.obs;
|
||||
final isRefreshing = false.obs;
|
||||
final penyewaList = <Map<String, dynamic>>[].obs;
|
||||
final filteredPenyewaList = <Map<String, dynamic>>[].obs;
|
||||
final filterStatus = 'all'.obs;
|
||||
final currentTabIndex = 0.obs;
|
||||
final searchQuery = ''.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchPenyewaList();
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
// Refresh data when the page is first loaded
|
||||
refreshData();
|
||||
}
|
||||
|
||||
// Method to refresh data when returning to the page
|
||||
void refreshData() {
|
||||
fetchPenyewaList();
|
||||
}
|
||||
|
||||
// Method khusus untuk pull-to-refresh yang tidak menampilkan loading spinner penuh layar
|
||||
Future<void> refreshPenyewaList() async {
|
||||
try {
|
||||
isRefreshing.value = true;
|
||||
|
||||
// Get all penyewa data without filtering
|
||||
final data =
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.select(
|
||||
'user_id, nama_lengkap, email, nik, no_hp, avatar, status, keterangan',
|
||||
)
|
||||
as List<dynamic>;
|
||||
|
||||
// Filter out rows where user_id is null
|
||||
final filteredData = data.where((row) => row['user_id'] != null).toList();
|
||||
|
||||
// Get total sewa count for each user
|
||||
final enrichedData = await _enrichWithSewaCount(filteredData);
|
||||
|
||||
penyewaList.value = enrichedData;
|
||||
|
||||
// Apply filters to update filteredPenyewaList
|
||||
applyFilters();
|
||||
|
||||
// Tampilkan pesan sukses
|
||||
} catch (e) {
|
||||
print('Error refreshing penyewa list: $e');
|
||||
} finally {
|
||||
isRefreshing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void changeTab(int index) {
|
||||
currentTabIndex.value = index;
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
void updateSearchQuery(String query) {
|
||||
searchQuery.value = query;
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
void applyFilters() {
|
||||
if (penyewaList.isEmpty) return;
|
||||
|
||||
// First apply status filter based on current tab
|
||||
String statusFilter;
|
||||
switch (currentTabIndex.value) {
|
||||
case 0: // Verifikasi
|
||||
statusFilter = 'pending';
|
||||
break;
|
||||
case 1: // Aktif
|
||||
statusFilter = 'active';
|
||||
break;
|
||||
case 2: // Ditangguhkan
|
||||
statusFilter = 'suspended';
|
||||
break;
|
||||
default:
|
||||
statusFilter = 'all';
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
var result =
|
||||
statusFilter == 'all'
|
||||
? penyewaList
|
||||
: penyewaList
|
||||
.where((p) => p['status']?.toLowerCase() == statusFilter)
|
||||
.toList();
|
||||
|
||||
// Then apply search filter if there's a query
|
||||
if (searchQuery.value.isNotEmpty) {
|
||||
final query = searchQuery.value.toLowerCase();
|
||||
result =
|
||||
result
|
||||
.where(
|
||||
(p) =>
|
||||
(p['nama_lengkap']?.toString().toLowerCase().contains(
|
||||
query,
|
||||
) ??
|
||||
false) ||
|
||||
(p['email']?.toString().toLowerCase().contains(query) ??
|
||||
false),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
filteredPenyewaList.value = result;
|
||||
}
|
||||
|
||||
Future<void> fetchPenyewaList() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// Get all penyewa data without filtering
|
||||
final data =
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.select(
|
||||
'user_id, nama_lengkap, email, nik, no_hp, avatar, status, keterangan',
|
||||
)
|
||||
as List<dynamic>;
|
||||
|
||||
// Filter out rows where user_id is null
|
||||
final filteredData = data.where((row) => row['user_id'] != null).toList();
|
||||
|
||||
// Get total sewa count for each user
|
||||
final enrichedData = await _enrichWithSewaCount(filteredData);
|
||||
|
||||
penyewaList.value = enrichedData;
|
||||
|
||||
// Apply filters to update filteredPenyewaList
|
||||
applyFilters();
|
||||
} catch (e) {
|
||||
print('Error fetching penyewa list: $e');
|
||||
penyewaList.value = [];
|
||||
filteredPenyewaList.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> _enrichWithSewaCount(
|
||||
List<dynamic> penyewaData,
|
||||
) async {
|
||||
final result = <Map<String, dynamic>>[];
|
||||
|
||||
for (var penyewa in penyewaData) {
|
||||
final userId = penyewa['user_id'];
|
||||
|
||||
// Count total sewa for this user
|
||||
final sewaCount = await _countUserSewa(userId);
|
||||
|
||||
// Create a new map with all the original data plus the total_sewa count
|
||||
final enrichedPenyewa = Map<String, dynamic>.from(penyewa);
|
||||
enrichedPenyewa['total_sewa'] = sewaCount;
|
||||
|
||||
result.add(enrichedPenyewa);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<int> _countUserSewa(String userId) async {
|
||||
try {
|
||||
final response = await _authProvider.client
|
||||
.from('sewa_aset')
|
||||
.select('id')
|
||||
.eq('user_id', userId);
|
||||
|
||||
return (response as List).length;
|
||||
} catch (e) {
|
||||
print('Error counting sewa for user $userId: $e');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void viewPenyewaDetail(String userId) {
|
||||
// Navigate to penyewa detail page (to be implemented)
|
||||
print('View detail for penyewa with ID: $userId');
|
||||
|
||||
// Get.toNamed(Routes.PETUGAS_PENYEWA_DETAIL, arguments: {'user_id': userId});
|
||||
}
|
||||
|
||||
void updatePenyewaStatus(
|
||||
String userId,
|
||||
String newStatus,
|
||||
String keterangan,
|
||||
) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.update({'status': newStatus, 'keterangan': keterangan})
|
||||
.eq('user_id', userId);
|
||||
|
||||
// Refresh the list
|
||||
await fetchPenyewaList();
|
||||
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Status penyewa berhasil diperbarui',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error updating penyewa status: $e');
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan saat memperbarui status penyewa',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../services/sewa_service.dart';
|
||||
import '../../../data/models/rental_booking_model.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
|
||||
class PetugasSewaController extends GetxController {
|
||||
// Reactive variables
|
||||
@ -7,7 +10,7 @@ class PetugasSewaController extends GetxController {
|
||||
final searchQuery = ''.obs;
|
||||
final orderIdQuery = ''.obs;
|
||||
final selectedStatusFilter = 'Semua'.obs;
|
||||
final filteredSewaList = <Map<String, dynamic>>[].obs;
|
||||
final filteredSewaList = <SewaModel>[].obs;
|
||||
|
||||
// Filter options
|
||||
final List<String> statusFilters = [
|
||||
@ -15,13 +18,19 @@ class PetugasSewaController extends GetxController {
|
||||
'Menunggu Pembayaran',
|
||||
'Periksa Pembayaran',
|
||||
'Diterima',
|
||||
'Aktif',
|
||||
'Dikembalikan',
|
||||
'Selesai',
|
||||
'Dibatalkan',
|
||||
];
|
||||
|
||||
// Mock data for sewa list
|
||||
final RxList<Map<String, dynamic>> sewaList = <Map<String, dynamic>>[].obs;
|
||||
final RxList<SewaModel> sewaList = <SewaModel>[].obs;
|
||||
|
||||
// Payment option state (per sewa)
|
||||
final Map<String, RxBool> isFullPaymentMap = {};
|
||||
final Map<String, TextEditingController> nominalControllerMap = {};
|
||||
final Map<String, RxString> paymentMethodMap = {};
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -41,26 +50,24 @@ class PetugasSewaController extends GetxController {
|
||||
void _updateFilteredList() {
|
||||
filteredSewaList.value =
|
||||
sewaList.where((sewa) {
|
||||
// Apply search filter
|
||||
final matchesSearch = sewa['nama_warga']
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.contains(searchQuery.value.toLowerCase());
|
||||
final query = searchQuery.value.toLowerCase();
|
||||
// Apply search filter: nama warga, id pesanan, atau asetId
|
||||
final matchesSearch =
|
||||
sewa.wargaNama.toLowerCase().contains(query) ||
|
||||
sewa.id.toLowerCase().contains(query) ||
|
||||
(sewa.asetId != null &&
|
||||
sewa.asetId!.toLowerCase().contains(query));
|
||||
|
||||
// Apply 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.toUpperCase() ==
|
||||
selectedStatusFilter.value.toUpperCase();
|
||||
|
||||
// Apply status filter if not 'Semua'
|
||||
final matchesStatus =
|
||||
selectedStatusFilter.value == 'Semua' ||
|
||||
sewa['status'] == selectedStatusFilter.value;
|
||||
|
||||
return matchesSearch && matchesOrderId && matchesStatus;
|
||||
}).toList();
|
||||
return matchesSearch && matchesStatus;
|
||||
}).toList()
|
||||
// Sort filtered results by tanggal_pemesanan in descending order (newest first)
|
||||
..sort((a, b) => b.tanggalPemesanan.compareTo(a.tanggalPemesanan));
|
||||
}
|
||||
|
||||
// Load sewa data (mock data for now)
|
||||
@ -68,100 +75,10 @@ class PetugasSewaController extends GetxController {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// Simulate API call delay
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
|
||||
// Populate with mock data
|
||||
sewaList.assignAll([
|
||||
{
|
||||
'id': '1',
|
||||
'order_id': 'SWA-001',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-02-05',
|
||||
'tanggal_selesai': '2025-02-10',
|
||||
'total_biaya': 45000,
|
||||
'status': 'Diterima',
|
||||
'photo_url': 'https://example.com/photo1.jpg',
|
||||
},
|
||||
{
|
||||
'id': '2',
|
||||
'order_id': 'SWA-002',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-02-15',
|
||||
'tanggal_selesai': '2025-02-20',
|
||||
'total_biaya': 30000,
|
||||
'status': 'Selesai',
|
||||
'photo_url': 'https://example.com/photo2.jpg',
|
||||
},
|
||||
{
|
||||
'id': '3',
|
||||
'order_id': 'SWA-003',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-02-25',
|
||||
'tanggal_selesai': '2025-03-01',
|
||||
'total_biaya': 35000,
|
||||
'status': 'Menunggu Pembayaran',
|
||||
'photo_url': 'https://example.com/photo3.jpg',
|
||||
},
|
||||
{
|
||||
'id': '4',
|
||||
'order_id': 'SWA-004',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-03-05',
|
||||
'tanggal_selesai': '2025-03-08',
|
||||
'total_biaya': 20000,
|
||||
'status': 'Periksa Pembayaran',
|
||||
'photo_url': 'https://example.com/photo4.jpg',
|
||||
},
|
||||
{
|
||||
'id': '5',
|
||||
'order_id': 'SWA-005',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-03-12',
|
||||
'tanggal_selesai': '2025-03-14',
|
||||
'total_biaya': 15000,
|
||||
'status': 'Dibatalkan',
|
||||
'photo_url': 'https://example.com/photo5.jpg',
|
||||
},
|
||||
{
|
||||
'id': '6',
|
||||
'order_id': 'SWA-006',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-03-18',
|
||||
'tanggal_selesai': '2025-03-20',
|
||||
'total_biaya': 25000,
|
||||
'status': 'Pembayaran Denda',
|
||||
'photo_url': 'https://example.com/photo6.jpg',
|
||||
},
|
||||
{
|
||||
'id': '7',
|
||||
'order_id': 'SWA-007',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-03-25',
|
||||
'tanggal_selesai': '2025-03-28',
|
||||
'total_biaya': 40000,
|
||||
'status': 'Periksa Denda',
|
||||
'photo_url': 'https://example.com/photo7.jpg',
|
||||
},
|
||||
{
|
||||
'id': '8',
|
||||
'order_id': 'SWA-008',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-04-02',
|
||||
'tanggal_selesai': '2025-04-05',
|
||||
'total_biaya': 10000,
|
||||
'status': 'Dikembalikan',
|
||||
'photo_url': 'https://example.com/photo8.jpg',
|
||||
},
|
||||
]);
|
||||
final data = await SewaService().fetchAllSewa();
|
||||
// Sort data by tanggal_pemesanan in descending order (newest first)
|
||||
data.sort((a, b) => b.tanggalPemesanan.compareTo(a.tanggalPemesanan));
|
||||
sewaList.assignAll(data);
|
||||
} catch (e) {
|
||||
print('Error loading sewa data: $e');
|
||||
} finally {
|
||||
@ -188,22 +105,27 @@ class PetugasSewaController extends GetxController {
|
||||
void resetFilters() {
|
||||
selectedStatusFilter.value = 'Semua';
|
||||
searchQuery.value = '';
|
||||
filteredSewaList.value = sewaList;
|
||||
// Assign a sorted copy of sewaList to filteredSewaList
|
||||
filteredSewaList.value = List<SewaModel>.from(sewaList)
|
||||
..sort((a, b) => b.tanggalPemesanan.compareTo(a.tanggalPemesanan));
|
||||
}
|
||||
|
||||
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();
|
||||
bool matchesStatus =
|
||||
selectedStatusFilter.value == 'Semua' ||
|
||||
sewa.status.toUpperCase() ==
|
||||
selectedStatusFilter.value.toUpperCase();
|
||||
bool matchesSearch =
|
||||
searchQuery.value.isEmpty ||
|
||||
sewa.wargaNama.toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
);
|
||||
return matchesStatus && matchesSearch;
|
||||
}).toList()
|
||||
// Sort filtered results by tanggal_pemesanan in descending order (newest first)
|
||||
..sort((a, b) => b.tanggalPemesanan.compareTo(a.tanggalPemesanan));
|
||||
}
|
||||
|
||||
// Format price to rupiah
|
||||
@ -213,102 +135,367 @@ class PetugasSewaController extends GetxController {
|
||||
|
||||
// Get color based on status
|
||||
Color getStatusColor(String status) {
|
||||
switch (status) {
|
||||
case 'Menunggu Pembayaran':
|
||||
return Colors.orange;
|
||||
case 'Periksa Pembayaran':
|
||||
return Colors.amber.shade700;
|
||||
case 'Diterima':
|
||||
return Colors.blue;
|
||||
case 'Pembayaran Denda':
|
||||
return Colors.deepOrange;
|
||||
case 'Periksa Denda':
|
||||
return Colors.red.shade600;
|
||||
case 'Dikembalikan':
|
||||
return Colors.teal;
|
||||
case 'Sedang Disewa':
|
||||
switch (status.toUpperCase()) {
|
||||
case 'MENUNGGU PEMBAYARAN':
|
||||
return Colors.orangeAccent;
|
||||
case 'PERIKSA PEMBAYARAN':
|
||||
return Colors.amber;
|
||||
case 'DITERIMA':
|
||||
return Colors.blueAccent;
|
||||
case 'AKTIF':
|
||||
return Colors.green;
|
||||
case 'Selesai':
|
||||
case 'PEMBAYARAN DENDA':
|
||||
return Colors.deepOrangeAccent;
|
||||
case 'PERIKSA PEMBAYARAN DENDA':
|
||||
return Colors.redAccent;
|
||||
case 'DIKEMBALIKAN':
|
||||
return Colors.teal;
|
||||
case 'SELESAI':
|
||||
return Colors.purple;
|
||||
case 'Dibatalkan':
|
||||
case 'DIBATALKAN':
|
||||
return Colors.red;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
// Get icon based on status
|
||||
IconData getStatusIcon(String status) {
|
||||
switch (status) {
|
||||
case 'MENUNGGU PEMBAYARAN':
|
||||
return Icons.payments_outlined;
|
||||
case 'PERIKSA PEMBAYARAN':
|
||||
return Icons.fact_check_outlined;
|
||||
case 'DITERIMA':
|
||||
return Icons.check_circle_outlined;
|
||||
case 'AKTIF':
|
||||
return Icons.play_circle_outline;
|
||||
case 'PEMBYARAN DENDA':
|
||||
return Icons.money_off_csred_outlined;
|
||||
case 'PERIKSA PEMBAYARAN DENDA':
|
||||
return Icons.assignment_late_outlined;
|
||||
case 'DIKEMBALIKAN':
|
||||
return Icons.assignment_return_outlined;
|
||||
case 'SELESAI':
|
||||
return Icons.task_alt_outlined;
|
||||
case 'DIBATALKAN':
|
||||
return Icons.cancel_outlined;
|
||||
default:
|
||||
return Icons.help_outline_rounded;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sewa approval (from "Periksa Pembayaran" to "Diterima")
|
||||
void approveSewa(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
final currentStatus = sewa['status'];
|
||||
|
||||
if (currentStatus == 'Periksa Pembayaran') {
|
||||
sewa['status'] = 'Diterima';
|
||||
} else if (currentStatus == 'Periksa Denda') {
|
||||
sewa['status'] = 'Selesai';
|
||||
} else if (currentStatus == 'Menunggu Pembayaran') {
|
||||
sewa['status'] = 'Periksa Pembayaran';
|
||||
final sewa = sewaList[index];
|
||||
final currentStatus = sewa.status;
|
||||
String? newStatus;
|
||||
if (currentStatus == 'PERIKSA PEMBAYARAN') {
|
||||
newStatus = 'DITERIMA';
|
||||
} else if (currentStatus == 'PERIKSA PEMBAYARAN DENDA') {
|
||||
newStatus = 'SELESAI';
|
||||
} else if (currentStatus == 'MENUNGGU PEMBAYARAN') {
|
||||
newStatus = 'PERIKSA PEMBAYARAN';
|
||||
}
|
||||
if (newStatus != null) {
|
||||
sewaList[index] = SewaModel(
|
||||
id: sewa.id,
|
||||
userId: sewa.userId,
|
||||
status: newStatus,
|
||||
waktuMulai: sewa.waktuMulai,
|
||||
waktuSelesai: sewa.waktuSelesai,
|
||||
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||
tipePesanan: sewa.tipePesanan,
|
||||
kuantitas: sewa.kuantitas,
|
||||
asetId: sewa.asetId,
|
||||
asetNama: sewa.asetNama,
|
||||
asetFoto: sewa.asetFoto,
|
||||
paketId: sewa.paketId,
|
||||
paketNama: sewa.paketNama,
|
||||
paketFoto: sewa.paketFoto,
|
||||
totalTagihan: sewa.totalTagihan,
|
||||
wargaNama: sewa.wargaNama,
|
||||
wargaNoHp: sewa.wargaNoHp,
|
||||
wargaAvatar: sewa.wargaAvatar,
|
||||
);
|
||||
sewaList.refresh();
|
||||
}
|
||||
|
||||
sewaList[index] = sewa;
|
||||
sewaList.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sewa rejection or cancellation
|
||||
void rejectSewa(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Dibatalkan';
|
||||
sewaList[index] = sewa;
|
||||
final sewa = sewaList[index];
|
||||
sewaList[index] = SewaModel(
|
||||
id: sewa.id,
|
||||
userId: sewa.userId,
|
||||
status: 'Dibatalkan',
|
||||
waktuMulai: sewa.waktuMulai,
|
||||
waktuSelesai: sewa.waktuSelesai,
|
||||
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||
tipePesanan: sewa.tipePesanan,
|
||||
kuantitas: sewa.kuantitas,
|
||||
asetId: sewa.asetId,
|
||||
asetNama: sewa.asetNama,
|
||||
asetFoto: sewa.asetFoto,
|
||||
paketId: sewa.paketId,
|
||||
paketNama: sewa.paketNama,
|
||||
paketFoto: sewa.paketFoto,
|
||||
totalTagihan: sewa.totalTagihan,
|
||||
wargaNama: sewa.wargaNama,
|
||||
wargaNoHp: sewa.wargaNoHp,
|
||||
wargaAvatar: sewa.wargaAvatar,
|
||||
);
|
||||
sewaList.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Request payment for penalty
|
||||
void requestPenaltyPayment(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Pembayaran Denda';
|
||||
sewaList[index] = sewa;
|
||||
final sewa = sewaList[index];
|
||||
sewaList[index] = SewaModel(
|
||||
id: sewa.id,
|
||||
userId: sewa.userId,
|
||||
status: 'Pembayaran Denda',
|
||||
waktuMulai: sewa.waktuMulai,
|
||||
waktuSelesai: sewa.waktuSelesai,
|
||||
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||
tipePesanan: sewa.tipePesanan,
|
||||
kuantitas: sewa.kuantitas,
|
||||
asetId: sewa.asetId,
|
||||
asetNama: sewa.asetNama,
|
||||
asetFoto: sewa.asetFoto,
|
||||
paketId: sewa.paketId,
|
||||
paketNama: sewa.paketNama,
|
||||
paketFoto: sewa.paketFoto,
|
||||
totalTagihan: sewa.totalTagihan,
|
||||
wargaNama: sewa.wargaNama,
|
||||
wargaNoHp: sewa.wargaNoHp,
|
||||
wargaAvatar: sewa.wargaAvatar,
|
||||
);
|
||||
sewaList.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Mark penalty payment as requiring inspection
|
||||
void markPenaltyForInspection(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Periksa Denda';
|
||||
sewaList[index] = sewa;
|
||||
final sewa = sewaList[index];
|
||||
sewaList[index] = SewaModel(
|
||||
id: sewa.id,
|
||||
userId: sewa.userId,
|
||||
status: 'Periksa Denda',
|
||||
waktuMulai: sewa.waktuMulai,
|
||||
waktuSelesai: sewa.waktuSelesai,
|
||||
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||
tipePesanan: sewa.tipePesanan,
|
||||
kuantitas: sewa.kuantitas,
|
||||
asetId: sewa.asetId,
|
||||
asetNama: sewa.asetNama,
|
||||
asetFoto: sewa.asetFoto,
|
||||
paketId: sewa.paketId,
|
||||
paketNama: sewa.paketNama,
|
||||
paketFoto: sewa.paketFoto,
|
||||
totalTagihan: sewa.totalTagihan,
|
||||
wargaNama: sewa.wargaNama,
|
||||
wargaNoHp: sewa.wargaNoHp,
|
||||
wargaAvatar: sewa.wargaAvatar,
|
||||
);
|
||||
sewaList.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sewa completion
|
||||
void completeSewa(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
void completeSewa(String id) async {
|
||||
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Selesai';
|
||||
sewaList[index] = sewa;
|
||||
final sewa = sewaList[index];
|
||||
sewaList[index] = SewaModel(
|
||||
id: sewa.id,
|
||||
userId: sewa.userId,
|
||||
status: 'Selesai',
|
||||
waktuMulai: sewa.waktuMulai,
|
||||
waktuSelesai: sewa.waktuSelesai,
|
||||
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||
tipePesanan: sewa.tipePesanan,
|
||||
kuantitas: sewa.kuantitas,
|
||||
asetId: sewa.asetId,
|
||||
asetNama: sewa.asetNama,
|
||||
asetFoto: sewa.asetFoto,
|
||||
paketId: sewa.paketId,
|
||||
paketNama: sewa.paketNama,
|
||||
paketFoto: sewa.paketFoto,
|
||||
totalTagihan: sewa.totalTagihan,
|
||||
wargaNama: sewa.wargaNama,
|
||||
wargaNoHp: sewa.wargaNoHp,
|
||||
wargaAvatar: sewa.wargaAvatar,
|
||||
);
|
||||
sewaList.refresh();
|
||||
// Update status in database
|
||||
final asetProvider = Get.find<AsetProvider>();
|
||||
await asetProvider.updateSewaAsetStatus(
|
||||
sewaAsetId: id,
|
||||
status: 'SELESAI',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark rental as returned
|
||||
void markAsReturned(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
Future<void> markAsReturned(String id) async {
|
||||
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Dikembalikan';
|
||||
sewaList[index] = sewa;
|
||||
final sewa = sewaList[index];
|
||||
sewaList[index] = SewaModel(
|
||||
id: sewa.id,
|
||||
userId: sewa.userId,
|
||||
status: 'Dikembalikan',
|
||||
waktuMulai: sewa.waktuMulai,
|
||||
waktuSelesai: sewa.waktuSelesai,
|
||||
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||
tipePesanan: sewa.tipePesanan,
|
||||
kuantitas: sewa.kuantitas,
|
||||
asetId: sewa.asetId,
|
||||
asetNama: sewa.asetNama,
|
||||
asetFoto: sewa.asetFoto,
|
||||
paketId: sewa.paketId,
|
||||
paketNama: sewa.paketNama,
|
||||
paketFoto: sewa.paketFoto,
|
||||
totalTagihan: sewa.totalTagihan,
|
||||
wargaNama: sewa.wargaNama,
|
||||
wargaNoHp: sewa.wargaNoHp,
|
||||
wargaAvatar: sewa.wargaAvatar,
|
||||
);
|
||||
sewaList.refresh();
|
||||
// Update status in database
|
||||
final asetProvider = Get.find<AsetProvider>();
|
||||
final result = await asetProvider.updateSewaAsetStatus(
|
||||
sewaAsetId: id,
|
||||
status: 'DIKEMBALIKAN',
|
||||
);
|
||||
if (!result) {
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Gagal mengubah status sewa di database',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil detail item paket (nama aset & kuantitas)
|
||||
Future<List<Map<String, dynamic>>> getPaketItems(String paketId) async {
|
||||
final asetProvider = Get.find<AsetProvider>();
|
||||
debugPrint('[DEBUG] getPaketItems called with paketId: $paketId');
|
||||
try {
|
||||
final items = await asetProvider.getPaketItems(paketId);
|
||||
debugPrint('[DEBUG] getPaketItems result for paketId $paketId:');
|
||||
for (var item in items) {
|
||||
debugPrint(' - item: ${item.toString()}');
|
||||
}
|
||||
return items;
|
||||
} catch (e, stack) {
|
||||
debugPrint('[ERROR] getPaketItems failed for paketId $paketId: $e');
|
||||
debugPrint('[ERROR] Stacktrace: $stack');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
RxBool getIsFullPayment(String sewaId) {
|
||||
if (!isFullPaymentMap.containsKey(sewaId)) {
|
||||
isFullPaymentMap[sewaId] = false.obs;
|
||||
}
|
||||
return isFullPaymentMap[sewaId]!;
|
||||
}
|
||||
|
||||
TextEditingController getNominalController(String sewaId) {
|
||||
if (!nominalControllerMap.containsKey(sewaId)) {
|
||||
final controller = TextEditingController(text: '0');
|
||||
nominalControllerMap[sewaId] = controller;
|
||||
}
|
||||
return nominalControllerMap[sewaId]!;
|
||||
}
|
||||
|
||||
void setFullPayment(String sewaId, bool value, num totalTagihan) {
|
||||
getIsFullPayment(sewaId).value = value;
|
||||
if (value) {
|
||||
getNominalController(sewaId).text = totalTagihan.toString();
|
||||
}
|
||||
}
|
||||
|
||||
RxString getPaymentMethod(String sewaId) {
|
||||
if (!paymentMethodMap.containsKey(sewaId)) {
|
||||
paymentMethodMap[sewaId] = 'Tunai'.obs;
|
||||
}
|
||||
return paymentMethodMap[sewaId]!;
|
||||
}
|
||||
|
||||
void setPaymentMethod(String sewaId, String method) {
|
||||
getPaymentMethod(sewaId).value = method;
|
||||
}
|
||||
|
||||
Future<String?> getTagihanSewaIdBySewaAsetId(String sewaAsetId) async {
|
||||
final asetProvider = Get.find<AsetProvider>();
|
||||
final tagihan = await asetProvider.getTagihanSewa(sewaAsetId);
|
||||
if (tagihan != null && tagihan['id'] != null) {
|
||||
return tagihan['id'] as String;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> confirmPembayaranTagihan({
|
||||
required String sewaAsetId,
|
||||
required int nominal,
|
||||
required String metodePembayaran,
|
||||
}) async {
|
||||
final tagihanSewaId = await getTagihanSewaIdBySewaAsetId(sewaAsetId);
|
||||
if (tagihanSewaId == null) {
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Tagihan sewa tidak ditemukan',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final asetProvider = Get.find<AsetProvider>();
|
||||
// Cek status sewa_aset saat ini
|
||||
final sewaAsetData = await asetProvider.getSewaAsetWithAsetData(sewaAsetId);
|
||||
if (sewaAsetData != null &&
|
||||
(sewaAsetData['status']?.toString()?.toUpperCase() ==
|
||||
'PERIKSA PEMBAYARAN')) {
|
||||
// Ubah status menjadi MENUNGGU PEMBAYARAN
|
||||
await asetProvider.updateSewaAsetStatus(
|
||||
sewaAsetId: sewaAsetId,
|
||||
status: 'MENUNGGU PEMBAYARAN',
|
||||
);
|
||||
}
|
||||
final result = await asetProvider.processPembayaranTagihan(
|
||||
tagihanSewaId: tagihanSewaId,
|
||||
nominal: nominal,
|
||||
metodePembayaran: metodePembayaran,
|
||||
);
|
||||
if (result) {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Pembayaran berhasil diproses',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Pembayaran gagal diproses',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,200 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:bumrent_app/app/data/models/aset_model.dart';
|
||||
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||
import 'package:bumrent_app/app/routes/app_routes.dart';
|
||||
|
||||
class PetugasTambahAsetController extends GetxController {
|
||||
// Flag to check if in edit mode
|
||||
final isEditing = false.obs;
|
||||
String? assetId; // To store the ID of the asset being edited
|
||||
|
||||
@override
|
||||
Future<void> onInit() async {
|
||||
super.onInit();
|
||||
|
||||
try {
|
||||
// Handle edit mode and load data if needed
|
||||
final args = Get.arguments;
|
||||
debugPrint(
|
||||
'[DEBUG] PetugasTambahAsetController initialized with args: $args',
|
||||
);
|
||||
|
||||
if (args != null && args is Map<String, dynamic>) {
|
||||
isEditing.value = args['isEditing'] ?? false;
|
||||
debugPrint('[DEBUG] isEditing set to: ${isEditing.value}');
|
||||
|
||||
if (isEditing.value) {
|
||||
// Get asset ID from arguments
|
||||
final assetId = args['assetId']?.toString() ?? '';
|
||||
debugPrint('[DEBUG] Edit mode: Loading asset with ID: $assetId');
|
||||
|
||||
if (assetId.isNotEmpty) {
|
||||
// Store the asset ID and load asset data
|
||||
this.assetId = assetId;
|
||||
debugPrint('[DEBUG] Asset ID set to: $assetId');
|
||||
|
||||
// Load asset data and await completion
|
||||
await _loadAssetData(assetId);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[ERROR] Edit mode but no assetId provided in arguments',
|
||||
);
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'ID Aset tidak ditemukan',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
// Optionally navigate back if in edit mode without an ID
|
||||
Future.delayed(Duration.zero, () => Get.back());
|
||||
}
|
||||
} else {
|
||||
// Set default values for new asset
|
||||
debugPrint('[DEBUG] Add new asset mode');
|
||||
quantityController.text = '1';
|
||||
unitOfMeasureController.text = 'Unit';
|
||||
}
|
||||
} else {
|
||||
// Default values for new asset when no arguments are passed
|
||||
debugPrint(
|
||||
'[DEBUG] No arguments passed, defaulting to add new asset mode',
|
||||
);
|
||||
quantityController.text = '1';
|
||||
unitOfMeasureController.text = 'Unit';
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('[ERROR] Error in onInit: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
// Ensure loading is set to false even if there's an error
|
||||
isLoading.value = false;
|
||||
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan saat memuat data',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
|
||||
// Listen to field changes for validation
|
||||
nameController.addListener(validateForm);
|
||||
descriptionController.addListener(validateForm);
|
||||
quantityController.addListener(validateForm);
|
||||
pricePerHourController.addListener(validateForm);
|
||||
pricePerDayController.addListener(validateForm);
|
||||
}
|
||||
|
||||
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
||||
final isLoading = false.obs;
|
||||
|
||||
Future<void> _loadAssetData(String assetId) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
debugPrint('[DEBUG] Fetching asset data for ID: $assetId');
|
||||
|
||||
// Fetch asset data from Supabase
|
||||
final aset = await _asetProvider.getAsetById(assetId);
|
||||
|
||||
if (aset == null) {
|
||||
throw Exception('Aset tidak ditemukan');
|
||||
}
|
||||
|
||||
debugPrint('[DEBUG] Successfully fetched asset data: ${aset.toJson()}');
|
||||
|
||||
// Populate form fields with the fetched data
|
||||
nameController.text = aset.nama ?? '';
|
||||
descriptionController.text = aset.deskripsi ?? '';
|
||||
quantityController.text = (aset.kuantitas ?? 1).toString();
|
||||
|
||||
// Ensure the status matches one of the available options exactly
|
||||
final status = aset.status?.toLowerCase() ?? 'tersedia';
|
||||
if (status == 'tersedia') {
|
||||
selectedStatus.value = 'Tersedia';
|
||||
} else if (status == 'pemeliharaan') {
|
||||
selectedStatus.value = 'Pemeliharaan';
|
||||
} else {
|
||||
// Default to 'Tersedia' if status is not recognized
|
||||
selectedStatus.value = 'Tersedia';
|
||||
}
|
||||
|
||||
// Handle time options and pricing
|
||||
if (aset.satuanWaktuSewa != null && aset.satuanWaktuSewa!.isNotEmpty) {
|
||||
// Reset time options
|
||||
timeOptions.forEach((key, value) => value.value = false);
|
||||
|
||||
// Process each satuan waktu sewa
|
||||
for (var sws in aset.satuanWaktuSewa) {
|
||||
final satuan =
|
||||
sws['nama_satuan_waktu']?.toString().toLowerCase() ?? '';
|
||||
final harga = sws['harga'] as int? ?? 0;
|
||||
final maksimalWaktu = sws['maksimal_waktu'] as int? ?? 24;
|
||||
|
||||
if (satuan.contains('jam')) {
|
||||
timeOptions['Per Jam']?.value = true;
|
||||
pricePerHourController.text = harga.toString();
|
||||
maxHourController.text = maksimalWaktu.toString();
|
||||
} else if (satuan.contains('hari')) {
|
||||
timeOptions['Per Hari']?.value = true;
|
||||
pricePerDayController.text = harga.toString();
|
||||
maxDayController.text = maksimalWaktu.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear existing images
|
||||
selectedImages.clear();
|
||||
networkImageUrls.clear();
|
||||
|
||||
// Get all image URLs from the model
|
||||
final allImageUrls = aset.imageUrls.toList();
|
||||
|
||||
// If no imageUrls but has imageUrl, use that as fallback (backward compatibility)
|
||||
if (allImageUrls.isEmpty &&
|
||||
aset.imageUrl != null &&
|
||||
aset.imageUrl!.isNotEmpty) {
|
||||
allImageUrls.add(aset.imageUrl!);
|
||||
}
|
||||
|
||||
// Add all images to the lists
|
||||
for (final imageUrl in allImageUrls) {
|
||||
if (imageUrl != null && imageUrl.isNotEmpty) {
|
||||
try {
|
||||
// For network images, we'll store the URL in networkImageUrls
|
||||
// and create a dummy XFile with the URL as path for backward compatibility
|
||||
final dummyFile = XFile(imageUrl);
|
||||
selectedImages.add(dummyFile);
|
||||
networkImageUrls.add(imageUrl);
|
||||
debugPrint('Added network image: $imageUrl');
|
||||
} catch (e) {
|
||||
debugPrint('Error adding network image: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'Total ${networkImageUrls.length} images loaded for asset $assetId',
|
||||
);
|
||||
debugPrint('[DEBUG] Successfully loaded asset data for ID: $assetId');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('[ERROR] Failed to load asset data: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memuat data aset: ${e.toString()}',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
// Optionally navigate back if there's an error
|
||||
Future.delayed(const Duration(seconds: 2), () => Get.back());
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Form controllers
|
||||
final nameController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
@ -23,28 +216,16 @@ class PetugasTambahAsetController extends GetxController {
|
||||
final categoryOptions = ['Sewa', 'Langganan'];
|
||||
final statusOptions = ['Tersedia', 'Pemeliharaan'];
|
||||
|
||||
// Images
|
||||
final selectedImages = <String>[].obs;
|
||||
// List to store selected images
|
||||
final RxList<XFile> selectedImages = <XFile>[].obs;
|
||||
// List to store network image URLs
|
||||
final RxList<String> networkImageUrls = <String>[].obs;
|
||||
final _picker = ImagePicker();
|
||||
|
||||
// Form validation
|
||||
final isFormValid = false.obs;
|
||||
final isSubmitting = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// Set default values
|
||||
quantityController.text = '1';
|
||||
unitOfMeasureController.text = 'Unit';
|
||||
|
||||
// Listen to field changes for validation
|
||||
nameController.addListener(validateForm);
|
||||
descriptionController.addListener(validateForm);
|
||||
quantityController.addListener(validateForm);
|
||||
pricePerHourController.addListener(validateForm);
|
||||
pricePerDayController.addListener(validateForm);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// Dispose controllers
|
||||
@ -89,17 +270,142 @@ class PetugasTambahAsetController extends GetxController {
|
||||
validateForm();
|
||||
}
|
||||
|
||||
// Add image to the list (in a real app, this would handle file upload)
|
||||
void addImage(String imagePath) {
|
||||
selectedImages.add(imagePath);
|
||||
validateForm();
|
||||
// Create a new asset in Supabase
|
||||
Future<String?> _createAsset(
|
||||
Map<String, dynamic> assetData,
|
||||
List<Map<String, dynamic>> satuanWaktuSewa,
|
||||
) async {
|
||||
try {
|
||||
// Create the asset in the 'aset' table
|
||||
final response = await _asetProvider.createAset(assetData);
|
||||
|
||||
if (response == null || response['id'] == null) {
|
||||
debugPrint('❌ Failed to create asset: No response or ID from server');
|
||||
return null;
|
||||
}
|
||||
|
||||
final String assetId = response['id'].toString();
|
||||
debugPrint('✅ Asset created with ID: $assetId');
|
||||
|
||||
// Add satuan waktu sewa
|
||||
for (var sws in satuanWaktuSewa) {
|
||||
final success = await _asetProvider.addSatuanWaktuSewa(
|
||||
asetId: assetId,
|
||||
satuanWaktu: sws['satuan_waktu'],
|
||||
harga: sws['harga'],
|
||||
maksimalWaktu: sws['maksimal_waktu'],
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
debugPrint('❌ Failed to add satuan waktu sewa: $sws');
|
||||
}
|
||||
}
|
||||
|
||||
return assetId;
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ Error creating asset: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove image from the list
|
||||
// Update an existing asset in Supabase
|
||||
Future<bool> _updateAsset(
|
||||
String assetId,
|
||||
Map<String, dynamic> assetData,
|
||||
List<Map<String, dynamic>> satuanWaktuSewa,
|
||||
) async {
|
||||
try {
|
||||
debugPrint('\n🔄 Starting update for asset ID: $assetId');
|
||||
|
||||
// 1. Extract and remove foto_aset from assetData as it's not in the aset table
|
||||
final fotoAsetUrl = assetData['foto_aset'];
|
||||
assetData.remove('foto_aset');
|
||||
debugPrint('📝 Asset data prepared for update (without foto_aset)');
|
||||
|
||||
// 2. Update the main asset data (without foto_aset)
|
||||
debugPrint('🔄 Updating main asset data...');
|
||||
final success = await _asetProvider.updateAset(assetId, assetData);
|
||||
if (!success) {
|
||||
debugPrint('❌ Failed to update asset with ID: $assetId');
|
||||
return false;
|
||||
}
|
||||
debugPrint('✅ Successfully updated main asset data');
|
||||
|
||||
// 3. Update satuan waktu sewa
|
||||
debugPrint('\n🔄 Updating rental time units...');
|
||||
// First, delete existing satuan waktu sewa
|
||||
await _asetProvider.deleteSatuanWaktuSewaByAsetId(assetId);
|
||||
|
||||
// Then add the new ones
|
||||
for (var sws in satuanWaktuSewa) {
|
||||
debugPrint(' - Adding: ${sws['satuan_waktu']} (${sws['harga']} IDR)');
|
||||
await _asetProvider.addSatuanWaktuSewa(
|
||||
asetId: assetId,
|
||||
satuanWaktu: sws['satuan_waktu'],
|
||||
harga: sws['harga'] as int,
|
||||
maksimalWaktu: sws['maksimal_waktu'] as int,
|
||||
);
|
||||
}
|
||||
debugPrint('✅ Successfully updated rental time units');
|
||||
|
||||
// 4. Update photos in the foto_aset table if any exist
|
||||
if (selectedImages.isNotEmpty || networkImageUrls.isNotEmpty) {
|
||||
// Combine network URLs and local file paths
|
||||
final List<String> allImageUrls = [
|
||||
...networkImageUrls,
|
||||
...selectedImages.map((file) => file.path),
|
||||
];
|
||||
|
||||
debugPrint('\n🖼️ Processing photos for asset $assetId');
|
||||
debugPrint(' - Network URLs: ${networkImageUrls.length}');
|
||||
debugPrint(' - Local files: ${selectedImages.length}');
|
||||
debugPrint(
|
||||
' - Total unique photos: ${allImageUrls.toSet().length} (before deduplication)',
|
||||
);
|
||||
|
||||
try {
|
||||
// Use updateFotoAset which handles both uploading new photos and updating the database
|
||||
final photoSuccess = await _asetProvider.updateFotoAset(
|
||||
asetId: assetId,
|
||||
fotoUrls: allImageUrls,
|
||||
);
|
||||
|
||||
if (!photoSuccess) {
|
||||
debugPrint(
|
||||
'⚠️ Some photos might not have been updated for asset $assetId',
|
||||
);
|
||||
// We don't fail the whole update if photo update fails
|
||||
// as the main asset data has been saved successfully
|
||||
} else {
|
||||
debugPrint('✅ Successfully updated photos for asset $assetId');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ Error updating photos: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
// Continue with the update even if photo update fails
|
||||
}
|
||||
} else {
|
||||
debugPrint('ℹ️ No photos to update');
|
||||
}
|
||||
|
||||
debugPrint('\n✅ Asset update completed successfully for ID: $assetId');
|
||||
return true;
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ Error updating asset: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove an image from the selected images list
|
||||
void removeImage(int index) {
|
||||
if (index >= 0 && index < selectedImages.length) {
|
||||
// Remove from both lists if they have an entry at this index
|
||||
if (index < networkImageUrls.length) {
|
||||
networkImageUrls.removeAt(index);
|
||||
}
|
||||
selectedImages.removeAt(index);
|
||||
validateForm();
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,62 +439,132 @@ class PetugasTambahAsetController extends GetxController {
|
||||
basicValid && perHourValid && perDayValid && anyTimeOptionSelected;
|
||||
}
|
||||
|
||||
// Submit form and save asset
|
||||
// Submit form and save or update asset
|
||||
Future<void> saveAsset() async {
|
||||
if (!isFormValid.value) return;
|
||||
|
||||
isSubmitting.value = true;
|
||||
|
||||
try {
|
||||
// In a real app, this would make an API call to save the asset
|
||||
await Future.delayed(const Duration(seconds: 1)); // Mock API call
|
||||
|
||||
// Prepare asset data
|
||||
final assetData = {
|
||||
// Prepare the basic asset data
|
||||
final Map<String, dynamic> assetData = {
|
||||
'nama': nameController.text,
|
||||
'deskripsi': descriptionController.text,
|
||||
'kategori': selectedCategory.value,
|
||||
'kategori': 'sewa', // Default to 'sewa' category
|
||||
'status': selectedStatus.value,
|
||||
'kuantitas': int.parse(quantityController.text),
|
||||
'satuan_ukur': unitOfMeasureController.text,
|
||||
'opsi_waktu_sewa':
|
||||
timeOptions.entries
|
||||
.where((entry) => entry.value.value)
|
||||
.map((entry) => entry.key)
|
||||
.toList(),
|
||||
'harga_per_jam':
|
||||
timeOptions['Per Jam']!.value
|
||||
? int.parse(pricePerHourController.text)
|
||||
: null,
|
||||
'max_jam':
|
||||
timeOptions['Per Jam']!.value && maxHourController.text.isNotEmpty
|
||||
? int.parse(maxHourController.text)
|
||||
: null,
|
||||
'harga_per_hari':
|
||||
timeOptions['Per Hari']!.value
|
||||
? int.parse(pricePerDayController.text)
|
||||
: null,
|
||||
'max_hari':
|
||||
timeOptions['Per Hari']!.value && maxDayController.text.isNotEmpty
|
||||
? int.parse(maxDayController.text)
|
||||
: null,
|
||||
'gambar': selectedImages,
|
||||
'satuan_ukur': 'unit', // Default unit of measure
|
||||
};
|
||||
|
||||
// Log the data (in a real app, this would be sent to an API)
|
||||
print('Asset data: $assetData');
|
||||
// Handle time options and pricing
|
||||
final List<Map<String, dynamic>> satuanWaktuSewa = [];
|
||||
|
||||
// Return to the asset list page
|
||||
Get.back();
|
||||
if (timeOptions['Per Jam']?.value == true) {
|
||||
final hargaPerJam = int.tryParse(pricePerHourController.text) ?? 0;
|
||||
final maxJam = int.tryParse(maxHourController.text) ?? 24;
|
||||
|
||||
// Show success message
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Aset berhasil ditambahkan',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
if (hargaPerJam <= 0) {
|
||||
throw Exception('Harga per jam harus lebih dari 0');
|
||||
}
|
||||
|
||||
satuanWaktuSewa.add({
|
||||
'satuan_waktu': 'jam',
|
||||
'harga': hargaPerJam,
|
||||
'maksimal_waktu': maxJam,
|
||||
});
|
||||
}
|
||||
|
||||
if (timeOptions['Per Hari']?.value == true) {
|
||||
final hargaPerHari = int.tryParse(pricePerDayController.text) ?? 0;
|
||||
final maxHari = int.tryParse(maxDayController.text) ?? 30;
|
||||
|
||||
if (hargaPerHari <= 0) {
|
||||
throw Exception('Harga per hari harus lebih dari 0');
|
||||
}
|
||||
|
||||
satuanWaktuSewa.add({
|
||||
'satuan_waktu': 'hari',
|
||||
'harga': hargaPerHari,
|
||||
'maksimal_waktu': maxHari,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate that at least one time option is selected
|
||||
if (satuanWaktuSewa.isEmpty) {
|
||||
throw Exception('Pilih setidaknya satu opsi waktu sewa (jam/hari)');
|
||||
}
|
||||
|
||||
// Handle image uploads
|
||||
List<String> imageUrls = [];
|
||||
|
||||
if (networkImageUrls.isNotEmpty) {
|
||||
// Use existing network URLs
|
||||
imageUrls = List.from(networkImageUrls);
|
||||
} else if (selectedImages.isNotEmpty) {
|
||||
// For local files, we'll upload them to Supabase Storage
|
||||
// Store the file paths for now, they'll be uploaded in the provider
|
||||
imageUrls = selectedImages.map((file) => file.path).toList();
|
||||
debugPrint('Found ${imageUrls.length} local images to upload');
|
||||
} else if (!isEditing.value) {
|
||||
// For new assets, require at least one image
|
||||
throw Exception('Harap unggah setidaknya satu gambar');
|
||||
}
|
||||
|
||||
// Ensure at least one image is provided for new assets
|
||||
if (imageUrls.isEmpty && !isEditing.value) {
|
||||
throw Exception('Harap unggah setidaknya satu gambar');
|
||||
}
|
||||
|
||||
// Create or update the asset
|
||||
bool success;
|
||||
String? createdAssetId;
|
||||
|
||||
if (isEditing.value && (assetId?.isNotEmpty ?? false)) {
|
||||
// Update existing asset
|
||||
debugPrint('🔄 Updating asset with ID: $assetId');
|
||||
success = await _updateAsset(assetId!, assetData, satuanWaktuSewa);
|
||||
|
||||
// Update all photos if we have any
|
||||
if (success && imageUrls.isNotEmpty) {
|
||||
await _asetProvider.updateFotoAset(
|
||||
asetId: assetId!,
|
||||
fotoUrls: imageUrls,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Create new asset
|
||||
debugPrint('🔄 Creating new asset');
|
||||
createdAssetId = await _createAsset(assetData, satuanWaktuSewa);
|
||||
success = createdAssetId != null;
|
||||
|
||||
// Add all photos for new asset
|
||||
if (success && createdAssetId != null && imageUrls.isNotEmpty) {
|
||||
await _asetProvider.updateFotoAset(
|
||||
asetId: createdAssetId,
|
||||
fotoUrls: imageUrls,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// Show success message
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
isEditing.value
|
||||
? 'Aset berhasil diperbarui'
|
||||
: 'Aset berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
|
||||
// Navigate back with success result instead of using offNamed
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
Get.offNamed(Routes.PETUGAS_ASET);
|
||||
} else {
|
||||
throw Exception('Gagal menyimpan aset');
|
||||
}
|
||||
} catch (e) {
|
||||
// Show error message
|
||||
Get.snackbar(
|
||||
@ -196,15 +572,76 @@ class PetugasTambahAsetController extends GetxController {
|
||||
'Terjadi kesalahan: ${e.toString()}',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Example method to upload images (to be implemented with your backend)
|
||||
// Future<List<String>> _uploadImages(List<XFile> images) async {
|
||||
// List<String> urls = [];
|
||||
// for (var image in images) {
|
||||
// // Upload image to your server and get the URL
|
||||
// // final url = await yourApiService.uploadImage(File(image.path));
|
||||
// // urls.add(url);
|
||||
// urls.add('https://example.com/path/to/uploaded/image.jpg'); // Mock URL
|
||||
// }
|
||||
// return urls;
|
||||
// }
|
||||
|
||||
// Pick image from camera
|
||||
Future<void> pickImageFromCamera() async {
|
||||
try {
|
||||
final XFile? image = await _picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 80,
|
||||
maxWidth: 1024,
|
||||
maxHeight: 1024,
|
||||
);
|
||||
if (image != null) {
|
||||
selectedImages.add(image);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal mengambil gambar dari kamera: $e',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Pick image from gallery
|
||||
Future<void> pickImageFromGallery() async {
|
||||
try {
|
||||
final List<XFile>? images = await _picker.pickMultiImage(
|
||||
imageQuality: 80,
|
||||
maxWidth: 1024,
|
||||
maxHeight: 1024,
|
||||
);
|
||||
if (images != null && images.isNotEmpty) {
|
||||
selectedImages.addAll(images);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memilih gambar dari galeri: $e',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// For demonstration purposes: add sample image
|
||||
void addSampleImage() {
|
||||
addImage('assets/images/sample_asset_${selectedImages.length + 1}.jpg');
|
||||
// In a real app, this would open the image picker
|
||||
selectedImages.add(
|
||||
XFile('assets/images/sample_asset_${selectedImages.length + 1}.jpg'),
|
||||
);
|
||||
validateForm();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:bumrent_app/app/data/models/paket_model.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||
import 'dart:io';
|
||||
import 'package:bumrent_app/app/routes/app_routes.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class PetugasTambahPaketController extends GetxController {
|
||||
// Form controllers
|
||||
@ -10,14 +17,14 @@ class PetugasTambahPaketController extends GetxController {
|
||||
|
||||
// Dropdown and toggle values
|
||||
final selectedCategory = 'Bulanan'.obs;
|
||||
final selectedStatus = 'Aktif'.obs;
|
||||
final selectedStatus = 'Tersedia'.obs;
|
||||
|
||||
// Category options
|
||||
final categoryOptions = ['Bulanan', 'Tahunan', 'Premium', 'Bisnis'];
|
||||
final statusOptions = ['Aktif', 'Nonaktif'];
|
||||
final statusOptions = ['Tersedia', 'Pemeliharaan'];
|
||||
|
||||
// Images
|
||||
final selectedImages = <String>[].obs;
|
||||
final selectedImages = <dynamic>[].obs;
|
||||
|
||||
// For package name and description
|
||||
final packageNameController = TextEditingController();
|
||||
@ -31,21 +38,90 @@ class PetugasTambahPaketController extends GetxController {
|
||||
// For asset selection
|
||||
final RxList<Map<String, dynamic>> availableAssets =
|
||||
<Map<String, dynamic>>[].obs;
|
||||
final Rx<int?> selectedAsset = Rx<int?>(null);
|
||||
final Rx<String?> selectedAsset = Rx<String?>(null);
|
||||
final RxBool isLoadingAssets = false.obs;
|
||||
|
||||
// Form validation
|
||||
final isFormValid = false.obs;
|
||||
final isSubmitting = false.obs;
|
||||
|
||||
// New RxBool for editing
|
||||
final isEditing = false.obs;
|
||||
|
||||
// New RxBool for viewing (read-only mode)
|
||||
final isViewing = false.obs;
|
||||
|
||||
final timeOptions = {'Per Jam': true.obs, 'Per Hari': false.obs};
|
||||
final pricePerHourController = TextEditingController();
|
||||
final maxHourController = TextEditingController();
|
||||
final pricePerDayController = TextEditingController();
|
||||
final maxDayController = TextEditingController();
|
||||
|
||||
final _picker = ImagePicker();
|
||||
|
||||
final isFormChanged = false.obs;
|
||||
Map<String, dynamic> initialFormData = {};
|
||||
|
||||
final AsetProvider _asetProvider = Get.put(AsetProvider());
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// Ambil flag isEditing dan isViewing dari arguments
|
||||
isEditing.value =
|
||||
Get.arguments != null && Get.arguments['isEditing'] == true;
|
||||
isViewing.value =
|
||||
Get.arguments != null && Get.arguments['isViewing'] == true;
|
||||
|
||||
if (isEditing.value || isViewing.value) {
|
||||
final paketArg = Get.arguments['paket'];
|
||||
String? paketId;
|
||||
if (paketArg != null) {
|
||||
if (paketArg is Map && paketArg['id'] != null) {
|
||||
paketId = paketArg['id'].toString();
|
||||
} else if (paketArg is PaketModel && paketArg.id != null) {
|
||||
paketId = paketArg.id.toString();
|
||||
}
|
||||
}
|
||||
if (paketId != null) {
|
||||
fetchPaketDetail(paketId);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen to field changes for validation
|
||||
nameController.addListener(validateForm);
|
||||
descriptionController.addListener(validateForm);
|
||||
priceController.addListener(validateForm);
|
||||
nameController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
descriptionController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
priceController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
itemQuantityController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
pricePerHourController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
maxHourController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
pricePerDayController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
maxDayController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
|
||||
// Load available assets when the controller initializes
|
||||
fetchAvailableAssets();
|
||||
@ -61,6 +137,10 @@ class PetugasTambahPaketController extends GetxController {
|
||||
packageNameController.dispose();
|
||||
packageDescriptionController.dispose();
|
||||
packagePriceController.dispose();
|
||||
pricePerHourController.dispose();
|
||||
maxHourController.dispose();
|
||||
pricePerDayController.dispose();
|
||||
maxDayController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@ -68,18 +148,21 @@ class PetugasTambahPaketController extends GetxController {
|
||||
void setCategory(String category) {
|
||||
selectedCategory.value = category;
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
}
|
||||
|
||||
// Change selected status
|
||||
void setStatus(String status) {
|
||||
selectedStatus.value = status;
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
}
|
||||
|
||||
// Add image to the list (in a real app, this would handle file upload)
|
||||
void addImage(String imagePath) {
|
||||
selectedImages.add(imagePath);
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
}
|
||||
|
||||
// Remove image from the list
|
||||
@ -87,34 +170,43 @@ class PetugasTambahPaketController extends GetxController {
|
||||
if (index >= 0 && index < selectedImages.length) {
|
||||
selectedImages.removeAt(index);
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch available assets from the API or local data
|
||||
void fetchAvailableAssets() {
|
||||
// Fetch available assets from Supabase and filter out already selected ones
|
||||
Future<void> fetchAvailableAssets() async {
|
||||
isLoadingAssets.value = true;
|
||||
|
||||
// This is a mock implementation - replace with actual API call
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
availableAssets.value = [
|
||||
{'id': 1, 'nama': 'Laptop Dell XPS', 'stok': 5},
|
||||
{'id': 2, 'nama': 'Proyektor Epson', 'stok': 3},
|
||||
{'id': 3, 'nama': 'Meja Kantor', 'stok': 10},
|
||||
{'id': 4, 'nama': 'Kursi Ergonomis', 'stok': 15},
|
||||
{'id': 5, 'nama': 'Printer HP LaserJet', 'stok': 2},
|
||||
{'id': 6, 'nama': 'AC Panasonic 1PK', 'stok': 8},
|
||||
];
|
||||
try {
|
||||
final allAssets = await _asetProvider.getSewaAsets();
|
||||
final selectedAsetIds =
|
||||
packageItems.map((item) => item['asetId'].toString()).toSet();
|
||||
// Only show assets not yet selected
|
||||
availableAssets.value =
|
||||
allAssets
|
||||
.where((aset) => !selectedAsetIds.contains(aset.id))
|
||||
.map(
|
||||
(aset) => {
|
||||
'id': aset.id,
|
||||
'nama': aset.nama,
|
||||
'stok': aset.kuantitas,
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
availableAssets.value = [];
|
||||
} finally {
|
||||
isLoadingAssets.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Set the selected asset
|
||||
void setSelectedAsset(int? assetId) {
|
||||
void setSelectedAsset(String? assetId) {
|
||||
selectedAsset.value = assetId;
|
||||
}
|
||||
|
||||
// Get remaining stock for an asset (considering current selections)
|
||||
int getRemainingStock(int assetId) {
|
||||
int getRemainingStock(String assetId) {
|
||||
// Find the asset in available assets
|
||||
final asset = availableAssets.firstWhere(
|
||||
(item) => item['id'] == assetId,
|
||||
@ -129,7 +221,7 @@ class PetugasTambahPaketController extends GetxController {
|
||||
// Calculate how many of this asset are already in the package
|
||||
int alreadySelected = 0;
|
||||
for (var item in packageItems) {
|
||||
if (item['asetId'] == assetId) {
|
||||
if (item['asetId'].toString() == assetId) {
|
||||
alreadySelected += item['jumlah'] as int;
|
||||
}
|
||||
}
|
||||
@ -144,7 +236,7 @@ class PetugasTambahPaketController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Pilih aset dan masukkan jumlah',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -165,7 +257,7 @@ class PetugasTambahPaketController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Jumlah harus lebih dari 0',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -178,7 +270,7 @@ class PetugasTambahPaketController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Jumlah melebihi stok yang tersedia',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -200,10 +292,12 @@ class PetugasTambahPaketController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Item berhasil ditambahkan ke paket',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
checkFormChanged();
|
||||
}
|
||||
|
||||
// Update an existing package item
|
||||
@ -212,7 +306,7 @@ class PetugasTambahPaketController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Pilih aset dan masukkan jumlah',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -233,7 +327,7 @@ class PetugasTambahPaketController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Jumlah harus lebih dari 0',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -261,7 +355,7 @@ class PetugasTambahPaketController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Jumlah melebihi stok yang tersedia',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -274,7 +368,7 @@ class PetugasTambahPaketController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Jumlah melebihi stok yang tersedia',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -297,19 +391,24 @@ class PetugasTambahPaketController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Item berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
checkFormChanged();
|
||||
}
|
||||
|
||||
// Remove an item from the package
|
||||
void removeItem(int index) {
|
||||
packageItems.removeAt(index);
|
||||
if (index >= 0 && index < packageItems.length) {
|
||||
packageItems.removeAt(index);
|
||||
checkFormChanged();
|
||||
}
|
||||
Get.snackbar(
|
||||
'Dihapus',
|
||||
'Item berhasil dihapus dari paket',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -319,10 +418,7 @@ class PetugasTambahPaketController extends GetxController {
|
||||
void validateForm() {
|
||||
// Basic validation
|
||||
bool basicValid =
|
||||
nameController.text.isNotEmpty &&
|
||||
descriptionController.text.isNotEmpty &&
|
||||
priceController.text.isNotEmpty &&
|
||||
int.tryParse(priceController.text) != null;
|
||||
nameController.text.isNotEmpty && descriptionController.text.isNotEmpty;
|
||||
|
||||
// Package should have at least one item
|
||||
bool hasItems = packageItems.isNotEmpty;
|
||||
@ -337,42 +433,207 @@ class PetugasTambahPaketController extends GetxController {
|
||||
isSubmitting.value = true;
|
||||
|
||||
try {
|
||||
// In a real app, this would make an API call to save the package
|
||||
await Future.delayed(const Duration(seconds: 1)); // Mock API call
|
||||
final supabase = Supabase.instance.client;
|
||||
if (isEditing.value) {
|
||||
// --- UPDATE LOGIC ---
|
||||
final paketArg = Get.arguments['paket'];
|
||||
final String paketId =
|
||||
paketArg is Map && paketArg['id'] != null
|
||||
? paketArg['id'].toString()
|
||||
: (paketArg is PaketModel && paketArg.id != null
|
||||
? paketArg.id.toString()
|
||||
: '');
|
||||
if (paketId.isEmpty) throw Exception('ID paket tidak ditemukan');
|
||||
|
||||
// Prepare package data
|
||||
final paketData = {
|
||||
'nama': nameController.text,
|
||||
'deskripsi': descriptionController.text,
|
||||
'kategori': selectedCategory.value,
|
||||
'status': selectedStatus.value == 'Aktif',
|
||||
'harga': int.parse(priceController.text),
|
||||
'gambar': selectedImages,
|
||||
'items': packageItems,
|
||||
};
|
||||
// 1. Update data utama paket
|
||||
await supabase
|
||||
.from('paket')
|
||||
.update({
|
||||
'nama': nameController.text,
|
||||
'deskripsi': descriptionController.text,
|
||||
'status': selectedStatus.value.toLowerCase(),
|
||||
})
|
||||
.eq('id', paketId);
|
||||
|
||||
// Log the data (in a real app, this would be sent to an API)
|
||||
print('Package data: $paketData');
|
||||
// 2. Update paket_item: hapus semua, insert ulang
|
||||
await supabase.from('paket_item').delete().eq('paket_id', paketId);
|
||||
for (var item in packageItems) {
|
||||
await supabase.from('paket_item').insert({
|
||||
'paket_id': paketId,
|
||||
'aset_id': item['asetId'],
|
||||
'kuantitas': item['jumlah'],
|
||||
});
|
||||
}
|
||||
|
||||
// Return to the package list page
|
||||
Get.back();
|
||||
// 3. Update satuan_waktu_sewa: hapus semua, insert ulang
|
||||
await supabase
|
||||
.from('satuan_waktu_sewa')
|
||||
.delete()
|
||||
.eq('paket_id', paketId);
|
||||
// Fetch satuan_waktu UUIDs
|
||||
final satuanWaktuList = await supabase
|
||||
.from('satuan_waktu')
|
||||
.select('id, nama_satuan_waktu');
|
||||
String? jamId;
|
||||
String? hariId;
|
||||
for (var sw in satuanWaktuList) {
|
||||
final nama = (sw['nama_satuan_waktu'] ?? '').toString().toLowerCase();
|
||||
if (nama.contains('jam')) jamId = sw['id'];
|
||||
if (nama.contains('hari')) hariId = sw['id'];
|
||||
}
|
||||
if (timeOptions['Per Jam']?.value == true && jamId != null) {
|
||||
await supabase.from('satuan_waktu_sewa').insert({
|
||||
'paket_id': paketId,
|
||||
'satuan_waktu_id': jamId,
|
||||
'harga': int.tryParse(pricePerHourController.text) ?? 0,
|
||||
'maksimal_waktu': int.tryParse(maxHourController.text) ?? 0,
|
||||
});
|
||||
}
|
||||
if (timeOptions['Per Hari']?.value == true && hariId != null) {
|
||||
await supabase.from('satuan_waktu_sewa').insert({
|
||||
'paket_id': paketId,
|
||||
'satuan_waktu_id': hariId,
|
||||
'harga': int.tryParse(pricePerDayController.text) ?? 0,
|
||||
'maksimal_waktu': int.tryParse(maxDayController.text) ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Show success message
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Paket berhasil ditambahkan',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
// 4. Update foto_aset
|
||||
// a. Ambil foto lama dari DB
|
||||
final oldPhotos = await supabase
|
||||
.from('foto_aset')
|
||||
.select('foto_aset')
|
||||
.eq('id_paket', paketId);
|
||||
final oldPhotoUrls =
|
||||
oldPhotos
|
||||
.map((e) => e['foto_aset']?.toString())
|
||||
.whereType<String>()
|
||||
.toSet();
|
||||
final newPhotoUrls =
|
||||
selectedImages
|
||||
.map((img) => img is String ? img : (img.path ?? ''))
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toSet();
|
||||
// b. Hapus foto yang dihapus user (dari DB dan storage)
|
||||
final removedPhotos = oldPhotoUrls.difference(newPhotoUrls);
|
||||
for (final url in removedPhotos) {
|
||||
await supabase
|
||||
.from('foto_aset')
|
||||
.delete()
|
||||
.eq('foto_aset', url)
|
||||
.eq('id_paket', paketId);
|
||||
await _asetProvider.deleteFileFromStorage(url);
|
||||
}
|
||||
// c. Tambah foto baru (upload jika perlu, insert ke DB)
|
||||
for (final img in selectedImages) {
|
||||
String url = '';
|
||||
if (img is String && img.startsWith('http')) {
|
||||
url = img;
|
||||
} else if (img is XFile) {
|
||||
final uploaded = await _asetProvider.uploadFileToStorage(
|
||||
File(img.path),
|
||||
);
|
||||
if (uploaded != null) url = uploaded;
|
||||
}
|
||||
if (url.isNotEmpty && !oldPhotoUrls.contains(url)) {
|
||||
await supabase.from('foto_aset').insert({
|
||||
'id_paket': paketId,
|
||||
'foto_aset': url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sukses
|
||||
Get.offNamed(Routes.PETUGAS_PAKET);
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Paket berhasil diperbarui',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
} else {
|
||||
// --- ADD LOGIC ---
|
||||
final uuid = Uuid();
|
||||
final String paketId = uuid.v4();
|
||||
// 1. Insert ke tabel paket
|
||||
await supabase.from('paket').insert({
|
||||
'id': paketId,
|
||||
'nama': nameController.text,
|
||||
'deskripsi': descriptionController.text,
|
||||
'status': selectedStatus.value.toLowerCase(),
|
||||
});
|
||||
// 2. Insert ke paket_item
|
||||
for (var item in packageItems) {
|
||||
await supabase.from('paket_item').insert({
|
||||
'paket_id': paketId,
|
||||
'aset_id': item['asetId'],
|
||||
'kuantitas': item['jumlah'],
|
||||
});
|
||||
}
|
||||
// 3. Insert ke satuan_waktu_sewa (ambil UUID satuan waktu)
|
||||
final satuanWaktuList = await supabase
|
||||
.from('satuan_waktu')
|
||||
.select('id, nama_satuan_waktu');
|
||||
String? jamId;
|
||||
String? hariId;
|
||||
for (var sw in satuanWaktuList) {
|
||||
final nama = (sw['nama_satuan_waktu'] ?? '').toString().toLowerCase();
|
||||
if (nama.contains('jam')) jamId = sw['id'];
|
||||
if (nama.contains('hari')) hariId = sw['id'];
|
||||
}
|
||||
if (timeOptions['Per Jam']?.value == true && jamId != null) {
|
||||
await supabase.from('satuan_waktu_sewa').insert({
|
||||
'paket_id': paketId,
|
||||
'satuan_waktu_id': jamId,
|
||||
'harga': int.tryParse(pricePerHourController.text) ?? 0,
|
||||
'maksimal_waktu': int.tryParse(maxHourController.text) ?? 0,
|
||||
});
|
||||
}
|
||||
if (timeOptions['Per Hari']?.value == true && hariId != null) {
|
||||
await supabase.from('satuan_waktu_sewa').insert({
|
||||
'paket_id': paketId,
|
||||
'satuan_waktu_id': hariId,
|
||||
'harga': int.tryParse(pricePerDayController.text) ?? 0,
|
||||
'maksimal_waktu': int.tryParse(maxDayController.text) ?? 0,
|
||||
});
|
||||
}
|
||||
// 4. Insert ke foto_aset (upload jika perlu)
|
||||
for (final img in selectedImages) {
|
||||
String url = '';
|
||||
if (img is String && img.startsWith('http')) {
|
||||
url = img;
|
||||
} else if (img is XFile) {
|
||||
final uploaded = await _asetProvider.uploadFileToStorage(
|
||||
File(img.path),
|
||||
);
|
||||
if (uploaded != null) url = uploaded;
|
||||
}
|
||||
if (url.isNotEmpty) {
|
||||
await supabase.from('foto_aset').insert({
|
||||
'id_paket': paketId,
|
||||
'foto_aset': url,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Sukses
|
||||
Get.back(result: true);
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Paket berhasil ditambahkan',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Show error message
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan: ${e.toString()}',
|
||||
'Terjadi kesalahan: \\${e.toString()}',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
@ -390,4 +651,215 @@ class PetugasTambahPaketController extends GetxController {
|
||||
selectedImages.add('https://example.com/sample_image.jpg');
|
||||
validateForm();
|
||||
}
|
||||
|
||||
void toggleTimeOption(String option) {
|
||||
timeOptions[option]?.value = !(timeOptions[option]?.value ?? false);
|
||||
// Ensure at least one option is selected
|
||||
bool anySelected = false;
|
||||
timeOptions.forEach((key, value) {
|
||||
if (value.value) anySelected = true;
|
||||
});
|
||||
if (!anySelected) {
|
||||
timeOptions[option]?.value = true;
|
||||
}
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
}
|
||||
|
||||
Future<void> fetchPaketDetail(String paketId) async {
|
||||
try {
|
||||
debugPrint('[DEBUG] Fetching paket detail for id: $paketId');
|
||||
final supabase = Supabase.instance.client;
|
||||
// 1) Ambil data paket utama
|
||||
final paketData =
|
||||
await supabase
|
||||
.from('paket')
|
||||
.select('id, nama, deskripsi, status')
|
||||
.eq('id', paketId)
|
||||
.single();
|
||||
debugPrint('[DEBUG] Paket data: ' + paketData.toString());
|
||||
|
||||
// 2) Ambil paket_item
|
||||
final paketItemData = await supabase
|
||||
.from('paket_item')
|
||||
.select('id, paket_id, aset_id, kuantitas')
|
||||
.eq('paket_id', paketId);
|
||||
debugPrint('[DEBUG] Paket item data: ' + paketItemData.toString());
|
||||
|
||||
// 3) Ambil satuan_waktu_sewa
|
||||
final swsData = await supabase
|
||||
.from('satuan_waktu_sewa')
|
||||
.select('id, paket_id, satuan_waktu_id, harga, maksimal_waktu')
|
||||
.eq('paket_id', paketId);
|
||||
debugPrint('[DEBUG] Satuan waktu sewa data: ' + swsData.toString());
|
||||
|
||||
// 4) Ambil semua satuan_waktu_id dari swsData
|
||||
final swIds = swsData.map((e) => e['satuan_waktu_id']).toSet().toList();
|
||||
final swData =
|
||||
swIds.isNotEmpty
|
||||
? await supabase
|
||||
.from('satuan_waktu')
|
||||
.select('id, nama_satuan_waktu')
|
||||
.inFilter('id', swIds)
|
||||
: [];
|
||||
debugPrint('[DEBUG] Satuan waktu data: ' + swData.toString());
|
||||
final Map satuanWaktuMap = {
|
||||
for (var sw in swData) sw['id']: sw['nama_satuan_waktu'],
|
||||
};
|
||||
|
||||
// 5) Ambil foto_aset
|
||||
final fotoData = await supabase
|
||||
.from('foto_aset')
|
||||
.select('id_paket, foto_aset')
|
||||
.eq('id_paket', paketId);
|
||||
debugPrint('[DEBUG] Foto aset data: ' + fotoData.toString());
|
||||
|
||||
// 6) Kumpulkan semua aset_id dari paketItemData
|
||||
final asetIds = paketItemData.map((e) => e['aset_id']).toSet().toList();
|
||||
final asetData =
|
||||
asetIds.isNotEmpty
|
||||
? await supabase
|
||||
.from('aset')
|
||||
.select('id, nama, kuantitas')
|
||||
.inFilter('id', asetIds)
|
||||
: [];
|
||||
debugPrint('[DEBUG] Aset data: ' + asetData.toString());
|
||||
final Map asetMap = {for (var a in asetData) a['id']: a};
|
||||
|
||||
// Prefill field controller
|
||||
nameController.text = paketData['nama']?.toString() ?? '';
|
||||
descriptionController.text = paketData['deskripsi']?.toString() ?? '';
|
||||
// Status mapping
|
||||
final statusDb =
|
||||
(paketData['status']?.toString().toLowerCase() ?? 'tersedia');
|
||||
selectedStatus.value =
|
||||
statusDb == 'pemeliharaan' ? 'Pemeliharaan' : 'Tersedia';
|
||||
|
||||
// Foto
|
||||
selectedImages.clear();
|
||||
if (fotoData.isNotEmpty) {
|
||||
for (var foto in fotoData) {
|
||||
final url = foto['foto_aset']?.toString();
|
||||
if (url != null && url.isNotEmpty) {
|
||||
selectedImages.add(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Item paket
|
||||
packageItems.clear();
|
||||
for (var item in paketItemData) {
|
||||
final aset = asetMap[item['aset_id']];
|
||||
packageItems.add({
|
||||
'asetId': item['aset_id'],
|
||||
'nama': aset != null ? aset['nama'] : '',
|
||||
'jumlah': item['kuantitas'],
|
||||
'stok': aset != null ? aset['kuantitas'] : 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Opsi waktu & harga sewa
|
||||
// Reset
|
||||
timeOptions['Per Jam']?.value = false;
|
||||
timeOptions['Per Hari']?.value = false;
|
||||
pricePerHourController.clear();
|
||||
maxHourController.clear();
|
||||
pricePerDayController.clear();
|
||||
maxDayController.clear();
|
||||
for (var sws in swsData) {
|
||||
final satuanNama =
|
||||
satuanWaktuMap[sws['satuan_waktu_id']]?.toString().toLowerCase() ??
|
||||
'';
|
||||
if (satuanNama.contains('jam')) {
|
||||
timeOptions['Per Jam']?.value = true;
|
||||
pricePerHourController.text = (sws['harga'] ?? '').toString();
|
||||
maxHourController.text = (sws['maksimal_waktu'] ?? '').toString();
|
||||
} else if (satuanNama.contains('hari')) {
|
||||
timeOptions['Per Hari']?.value = true;
|
||||
pricePerDayController.text = (sws['harga'] ?? '').toString();
|
||||
maxDayController.text = (sws['maksimal_waktu'] ?? '').toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Simpan snapshot initialFormData setelah prefill
|
||||
initialFormData = {
|
||||
'nama': nameController.text,
|
||||
'deskripsi': descriptionController.text,
|
||||
'status': selectedStatus.value,
|
||||
'images': List.from(selectedImages),
|
||||
'items': List.from(packageItems),
|
||||
'perJam': timeOptions['Per Jam']?.value ?? false,
|
||||
'perHari': timeOptions['Per Hari']?.value ?? false,
|
||||
'hargaJam': pricePerHourController.text,
|
||||
'maxJam': maxHourController.text,
|
||||
'hargaHari': pricePerDayController.text,
|
||||
'maxHari': maxDayController.text,
|
||||
};
|
||||
isFormChanged.value = false;
|
||||
} catch (e, st) {
|
||||
debugPrint('[ERROR] Gagal fetch paket detail: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pickImageFromCamera() async {
|
||||
try {
|
||||
final XFile? image = await _picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 80,
|
||||
maxWidth: 1024,
|
||||
maxHeight: 1024,
|
||||
);
|
||||
if (image != null) {
|
||||
selectedImages.add(image);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal mengambil gambar dari kamera: $e',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pickImageFromGallery() async {
|
||||
try {
|
||||
final List<XFile>? images = await _picker.pickMultiImage(
|
||||
imageQuality: 80,
|
||||
maxWidth: 1024,
|
||||
maxHeight: 1024,
|
||||
);
|
||||
if (images != null && images.isNotEmpty) {
|
||||
for (final img in images) {
|
||||
selectedImages.add(img);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memilih gambar dari galeri: $e',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void checkFormChanged() {
|
||||
final current = {
|
||||
'nama': nameController.text,
|
||||
'deskripsi': descriptionController.text,
|
||||
'status': selectedStatus.value,
|
||||
'images': List.from(selectedImages),
|
||||
'items': List.from(packageItems),
|
||||
'perJam': timeOptions['Per Jam']?.value ?? false,
|
||||
'perHari': timeOptions['Per Hari']?.value ?? false,
|
||||
'hargaJam': pricePerHourController.text,
|
||||
'maxJam': maxHourController.text,
|
||||
'hargaHari': pricePerDayController.text,
|
||||
'maxHari': maxDayController.text,
|
||||
};
|
||||
isFormChanged.value = current.toString() != initialFormData.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@ -476,7 +476,7 @@ class ListPetugasMitraView extends GetView<ListPetugasMitraController> {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Harap isi semua data',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -600,7 +600,7 @@ class ListPetugasMitraView extends GetView<ListPetugasMitraController> {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Harap isi semua data',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
@ -203,7 +203,7 @@ class ListTagihanPeriodeView extends GetView<ListTagihanPeriodeController> {
|
||||
backgroundColor: Colors.orange.withOpacity(0.1),
|
||||
colorText: Colors.orange.shade800,
|
||||
duration: const Duration(seconds: 3),
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
margin: const EdgeInsets.all(8),
|
||||
);
|
||||
},
|
||||
|
||||
365
lib/app/modules/petugas_bumdes/views/petugas_akun_bank_view.dart
Normal file
@ -0,0 +1,365 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_akun_bank_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
|
||||
class PetugasAkunBankView extends GetView<PetugasAkunBankController> {
|
||||
const PetugasAkunBankView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get dashboard controller for side navbar
|
||||
final dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Kelola Akun Bank'),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.errorMessage.value.isNotEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 48, color: Colors.red[300]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
controller.errorMessage.value,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: controller.loadBankAccounts,
|
||||
child: const Text('Coba Lagi'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.bankAccounts.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.account_balance, size: 48, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Belum ada akun bank',
|
||||
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showAddEditBankAccountDialog(context),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Tambah Akun Bank'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
RefreshIndicator(
|
||||
onRefresh: controller.loadBankAccounts,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.bankAccounts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final account = controller.bankAccounts[index];
|
||||
return _buildBankAccountCard(context, account);
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: FloatingActionButton(
|
||||
onPressed: () => _showAddEditBankAccountDialog(context),
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBankAccountCard(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> account,
|
||||
) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.blueGrotto.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.account_balance,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
account['nama_bank'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
account['nama_akun'] ?? '',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
_showAddEditBankAccountDialog(context, account);
|
||||
} else if (value == 'delete') {
|
||||
_showDeleteConfirmationDialog(context, account);
|
||||
}
|
||||
},
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.edit, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('Edit'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete, size: 18, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Hapus',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.credit_card, size: 16, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'No. Rekening: ${account['no_rekening'] ?? ''}',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddEditBankAccountDialog(
|
||||
BuildContext context, [
|
||||
Map<String, dynamic>? account,
|
||||
]) {
|
||||
final isEditing = account != null;
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
String bankName = account?['nama_bank'] ?? '';
|
||||
String accountName = account?['nama_akun'] ?? '';
|
||||
String accountNumber = account?['no_rekening'] ?? '';
|
||||
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isEditing ? 'Edit Akun Bank' : 'Tambah Akun Bank',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
initialValue: bankName,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nama Bank',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.account_balance),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Nama bank tidak boleh kosong';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) => bankName = value,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
initialValue: accountName,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nama Pemilik Rekening',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.person),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Nama pemilik rekening tidak boleh kosong';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) => accountName = value,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
initialValue: accountNumber,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nomor Rekening',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.credit_card),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Nomor rekening tidak boleh kosong';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) => accountNumber = value,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (formKey.currentState!.validate()) {
|
||||
final accountData = {
|
||||
'nama_bank': bankName,
|
||||
'nama_akun': accountName,
|
||||
'no_rekening': accountNumber,
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
controller.updateBankAccount(
|
||||
account['id'],
|
||||
accountData,
|
||||
);
|
||||
} else {
|
||||
controller.addBankAccount(accountData);
|
||||
}
|
||||
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: Text(isEditing ? 'Simpan' : 'Tambah'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmationDialog(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> account,
|
||||
) {
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: const Text('Konfirmasi Hapus'),
|
||||
content: Text(
|
||||
'Apakah Anda yakin ingin menghapus akun bank ${account['nama_bank']} - ${account['nama_akun']}?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Get.back(), child: const Text('Batal')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
controller.deleteBankAccount(account['id']);
|
||||
Get.back();
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_aset_controller.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
@ -23,26 +24,12 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = Get.find<PetugasAsetController>();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
|
||||
// Listen to tab changes and update controller
|
||||
_tabController.addListener(() {
|
||||
if (!_tabController.indexIsChanging) {
|
||||
controller.changeTab(_tabController.index);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to controller tab changes and update TabController
|
||||
ever(controller.selectedTabIndex, (index) {
|
||||
if (_tabController.index != index) {
|
||||
_tabController.animateTo(index);
|
||||
}
|
||||
});
|
||||
// Initialize with default tab (sewa)
|
||||
controller.changeTab(0);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -82,7 +69,7 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
body: Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
_buildTabBar(),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(child: _buildAssetList()),
|
||||
],
|
||||
),
|
||||
@ -93,7 +80,17 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_ASET),
|
||||
onPressed: () {
|
||||
// Navigate to PetugasTambahAsetView in add mode and refresh data when returning
|
||||
Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_ASET,
|
||||
arguments: {'isEditing': false, 'assetData': null},
|
||||
)?.then((_) {
|
||||
// Refresh data when returning from tambah_aset page
|
||||
debugPrint('Returning from tambah aset page, refreshing data...');
|
||||
controller.loadAsetData();
|
||||
});
|
||||
},
|
||||
backgroundColor: AppColorsPetugas.babyBlueBright,
|
||||
icon: Icon(Icons.add, color: AppColorsPetugas.blueGrotto),
|
||||
label: Text(
|
||||
@ -144,60 +141,19 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: AppColorsPetugas.textSecondary,
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicator: BoxDecoration(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
dividerColor: Colors.transparent,
|
||||
tabs: const [
|
||||
Tab(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.shopping_cart, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('Sewa', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.subscriptions, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Langganan',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// Tab bar has been removed as per requirements
|
||||
|
||||
Widget _buildAssetList() {
|
||||
return Obx(() {
|
||||
debugPrint('_buildAssetList: isLoading=${controller.isLoading.value}');
|
||||
debugPrint(
|
||||
'_buildAssetList: filteredAsetList length=${controller.filteredAsetList.length}',
|
||||
);
|
||||
if (controller.filteredAsetList.isNotEmpty) {
|
||||
debugPrint(
|
||||
'_buildAssetList: First item name=${controller.filteredAsetList[0]['nama']}',
|
||||
);
|
||||
}
|
||||
if (controller.isLoading.value) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
@ -255,10 +211,15 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredAsetList.length,
|
||||
itemCount: controller.filteredAsetList.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
final aset = controller.filteredAsetList[index];
|
||||
return _buildAssetCard(context, aset);
|
||||
if (index < controller.filteredAsetList.length) {
|
||||
final aset = controller.filteredAsetList[index];
|
||||
return _buildAssetCard(context, aset);
|
||||
} else {
|
||||
// Blank space at the end
|
||||
return const SizedBox(height: 80);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -266,7 +227,31 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
}
|
||||
|
||||
Widget _buildAssetCard(BuildContext context, Map<String, dynamic> aset) {
|
||||
final isAvailable = aset['tersedia'] == true;
|
||||
debugPrint('\n--- Building Asset Card ---');
|
||||
debugPrint('Asset data: $aset');
|
||||
|
||||
// Extract and validate all asset properties with proper null safety
|
||||
final status =
|
||||
aset['status']?.toString().toLowerCase() ?? 'tidak_diketahui';
|
||||
final isAvailable = status == 'tersedia';
|
||||
final imageUrl = aset['imageUrl']?.toString() ?? '';
|
||||
final harga =
|
||||
aset['harga'] is int
|
||||
? aset['harga'] as int
|
||||
: (int.tryParse(aset['harga']?.toString() ?? '0') ?? 0);
|
||||
final satuanWaktu =
|
||||
aset['satuan_waktu']?.toString().capitalizeFirst ?? 'Hari';
|
||||
final nama = aset['nama']?.toString().trim() ?? 'Nama tidak tersedia';
|
||||
final kategori = aset['kategori']?.toString().trim() ?? 'Umum';
|
||||
final orderId = aset['order_id']?.toString() ?? '';
|
||||
|
||||
// Debug prints for development
|
||||
debugPrint('Image URL: $imageUrl');
|
||||
debugPrint('Harga: $harga');
|
||||
debugPrint('Satuan Waktu: $satuanWaktu');
|
||||
debugPrint('Nama: $nama');
|
||||
debugPrint('Kategori: $kategori');
|
||||
debugPrint('Status: $status (Available: $isAvailable)');
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
@ -286,25 +271,49 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _showAssetDetails(context, aset),
|
||||
child: Row(
|
||||
children: [
|
||||
// Asset image
|
||||
Container(
|
||||
SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
bottomLeft: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getAssetIcon(aset['kategori']),
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
size: 32,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder:
|
||||
(context, url) => Container(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getAssetIcon(
|
||||
kategori,
|
||||
), // Show category icon as placeholder
|
||||
color: AppColorsPetugas.navyBlue.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
errorWidget:
|
||||
(context, url, error) => Container(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons
|
||||
.broken_image, // Or your preferred error icon
|
||||
color: AppColorsPetugas.navyBlue.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -323,8 +332,8 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
aset['nama'],
|
||||
style: TextStyle(
|
||||
nama,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
@ -333,12 +342,63 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${controller.formatPrice(aset['harga'])} ${aset['satuan']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
// Harga dan satuan waktu (multi-line, tampilkan semua dari satuanWaktuSewa)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final satuanWaktuList =
|
||||
(aset['satuanWaktuSewa'] is List)
|
||||
? List<Map<String, dynamic>>.from(
|
||||
aset['satuanWaktuSewa'],
|
||||
)
|
||||
: [];
|
||||
final validSatuanWaktu =
|
||||
satuanWaktuList
|
||||
.where(
|
||||
(sw) =>
|
||||
(sw['harga'] ?? 0) > 0 &&
|
||||
(sw['nama_satuan_waktu'] !=
|
||||
null &&
|
||||
(sw['nama_satuan_waktu']
|
||||
as String)
|
||||
.isNotEmpty),
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (validSatuanWaktu.isNotEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children:
|
||||
validSatuanWaktu.map((sw) {
|
||||
final harga = sw['harga'] ?? 0;
|
||||
final satuan =
|
||||
sw['nama_satuan_waktu'] ?? '';
|
||||
return Text(
|
||||
'${controller.formatPrice(harga)} / $satuan',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
AppColorsPetugas
|
||||
.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
} else {
|
||||
// fallback: harga tunggal
|
||||
return Text(
|
||||
'${controller.formatPrice(aset['harga'] ?? 0)} / ${aset['satuan_waktu']?.toString().capitalizeFirst ?? 'Hari'}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -383,11 +443,42 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
children: [
|
||||
// Edit icon
|
||||
GestureDetector(
|
||||
onTap:
|
||||
() => _showAddEditAssetDialog(
|
||||
context,
|
||||
aset: aset,
|
||||
),
|
||||
onTap: () {
|
||||
// Navigate to PetugasTambahAsetView in edit mode with only the asset ID
|
||||
final assetId =
|
||||
aset['id']?.toString() ??
|
||||
''; // Changed from 'id_aset' to 'id'
|
||||
debugPrint(
|
||||
'[DEBUG] Navigating to edit asset with ID: $assetId',
|
||||
);
|
||||
debugPrint(
|
||||
'[DEBUG] Full asset data: $aset',
|
||||
); // Log full asset data for debugging
|
||||
|
||||
if (assetId.isEmpty) {
|
||||
debugPrint('[ERROR] Asset ID is empty!');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'ID Aset tidak valid',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_ASET,
|
||||
arguments: {
|
||||
'isEditing': true,
|
||||
'assetId': assetId,
|
||||
},
|
||||
)?.then((_) {
|
||||
// Refresh data when returning from edit page
|
||||
debugPrint(
|
||||
'Returning from edit aset page, refreshing data...',
|
||||
);
|
||||
controller.loadAsetData();
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
@ -589,590 +680,16 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
}
|
||||
}
|
||||
|
||||
void _showAssetDetails(BuildContext context, Map<String, dynamic> aset) {
|
||||
final isAvailable = aset['tersedia'] == true;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.85,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Header with gradient
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppColorsPetugas.blueGrotto,
|
||||
AppColorsPetugas.navyBlue,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Close button and availability badge
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isAvailable
|
||||
? AppColorsPetugas.successLight
|
||||
: AppColorsPetugas.errorLight,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color:
|
||||
isAvailable
|
||||
? AppColorsPetugas.success
|
||||
: AppColorsPetugas.error,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isAvailable ? Icons.check_circle : Icons.cancel,
|
||||
color:
|
||||
isAvailable
|
||||
? AppColorsPetugas.success
|
||||
: AppColorsPetugas.error,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
isAvailable ? 'Tersedia' : 'Tidak Tersedia',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
isAvailable
|
||||
? AppColorsPetugas.success
|
||||
: AppColorsPetugas.error,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Category badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
aset['kategori'],
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Asset name
|
||||
Text(
|
||||
aset['nama'],
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Price
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.monetization_on,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${controller.formatPrice(aset['harga'])} ${aset['satuan']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Asset details
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Quick info cards
|
||||
Row(
|
||||
children: [
|
||||
_buildInfoCard(
|
||||
Icons.inventory_2,
|
||||
'Stok',
|
||||
'${aset['stok']} unit',
|
||||
flex: 1,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildInfoCard(
|
||||
Icons.category,
|
||||
'Jenis',
|
||||
aset['jenis'],
|
||||
flex: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Description section
|
||||
Text(
|
||||
'Deskripsi',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
aset['deskripsi'],
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Action buttons
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColorsPetugas.shadowColor,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_showAddEditAssetDialog(context, aset: aset);
|
||||
},
|
||||
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),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_showDeleteConfirmation(context, aset);
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
label: const Text('Hapus'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.error,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoCard(
|
||||
IconData icon,
|
||||
String label,
|
||||
String value, {
|
||||
int flex = 1,
|
||||
}) {
|
||||
return Expanded(
|
||||
flex: flex,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColorsPetugas.babyBlue),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: AppColorsPetugas.blueGrotto),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailItem(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddEditAssetDialog(
|
||||
BuildContext context, {
|
||||
Map<String, dynamic>? aset,
|
||||
}) {
|
||||
final isEditing = aset != null;
|
||||
final jenisOptions = ['Sewa', 'Langganan'];
|
||||
final typeOptions = ['Elektronik', 'Furniture', 'Kendaraan', 'Lainnya'];
|
||||
|
||||
// In a real app, this would have proper form handling with controllers
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlue,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
isEditing ? Icons.edit : Icons.add,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isEditing ? 'Edit Aset' : 'Tambah Aset Baru',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Silakan lengkapi form di bawah ini',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Mock form - In a real app this would have actual form fields
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColorsPetugas.babyBlue),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Form pengelolaan aset akan ditampilkan di sini dengan field untuk:',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildMockFormField('Nama Aset', 'Contoh: Meja Rapat'),
|
||||
_buildMockFormField('Kategori', 'Pilih kategori aset'),
|
||||
_buildMockFormField(
|
||||
'Harga',
|
||||
'Masukkan harga per unit/periode',
|
||||
),
|
||||
_buildMockFormField(
|
||||
'Satuan',
|
||||
'Contoh: per hari, per bulan',
|
||||
),
|
||||
_buildMockFormField('Stok', 'Jumlah unit tersedia'),
|
||||
_buildMockFormField(
|
||||
'Deskripsi',
|
||||
'Keterangan lengkap aset',
|
||||
),
|
||||
_buildMockToggle(
|
||||
'Status Ketersediaan',
|
||||
isEditing && aset?['tersedia'] == true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Batal',
|
||||
style: TextStyle(
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
// In a real app, we would save the form data
|
||||
Get.snackbar(
|
||||
isEditing ? 'Aset Diperbarui' : 'Aset Ditambahkan',
|
||||
isEditing
|
||||
? 'Aset berhasil diperbarui'
|
||||
: 'Aset baru berhasil ditambahkan',
|
||||
backgroundColor: AppColorsPetugas.success,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(16),
|
||||
borderRadius: 10,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(isEditing ? 'Simpan' : 'Tambah'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMockFormField(String label, String hint) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColorsPetugas.babyBlue),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
hint,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.textLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMockToggle(String label, bool value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: value,
|
||||
onChanged: (_) {},
|
||||
activeColor: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
void _showAddEditAssetDialog(BuildContext context) {
|
||||
// Navigate to PetugasTambahAsetView in add mode and refresh data when returning
|
||||
Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_ASET,
|
||||
arguments: {'isEditing': false, 'assetData': null},
|
||||
)?.then((_) {
|
||||
// Refresh data when returning from tambah_aset page
|
||||
debugPrint('Returning from tambah aset page, refreshing data...');
|
||||
controller.loadAsetData();
|
||||
});
|
||||
}
|
||||
|
||||
void _showDeleteConfirmation(
|
||||
@ -1251,22 +768,11 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
controller.deleteAset(aset['id']);
|
||||
Get.snackbar(
|
||||
'Aset Dihapus',
|
||||
'Aset berhasil dihapus dari sistem',
|
||||
backgroundColor: AppColorsPetugas.error,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(16),
|
||||
borderRadius: 10,
|
||||
icon: const Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
// Let the controller handle the deletion and showing the snackbar
|
||||
await controller.deleteAset(aset['id']);
|
||||
// The controller will show appropriate success or error messages
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.error,
|
||||
|
||||
@ -327,7 +327,7 @@ class PetugasBumdesCbpView extends GetView<PetugasBumdesCbpController> {
|
||||
leading: const Icon(Icons.subscriptions_outlined),
|
||||
title: const Text('Kelola Langganan'),
|
||||
onTap: () {
|
||||
Get.offAllNamed(Routes.PETUGAS_LANGGANAN);
|
||||
Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
||||
@ -5,6 +5,8 @@ import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../../../utils/format_utils.dart';
|
||||
import '../views/petugas_penyewa_view.dart';
|
||||
|
||||
class PetugasBumdesDashboardView
|
||||
extends GetView<PetugasBumdesDashboardController> {
|
||||
@ -23,12 +25,7 @@ class PetugasBumdesDashboardView
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => _showLogoutConfirmation(context),
|
||||
),
|
||||
],
|
||||
// actions: [],
|
||||
),
|
||||
drawer: PetugasSideNavbar(controller: controller),
|
||||
drawerEdgeDragWidth: 60,
|
||||
@ -68,6 +65,8 @@ class PetugasBumdesDashboardView
|
||||
case 3:
|
||||
return 'Permintaan Sewa';
|
||||
case 4:
|
||||
return 'Penyewa';
|
||||
case 5:
|
||||
return 'Profil BUMDes';
|
||||
default:
|
||||
return 'Dashboard Petugas BUMDES';
|
||||
@ -85,6 +84,8 @@ class PetugasBumdesDashboardView
|
||||
case 3:
|
||||
return _buildSewaTab();
|
||||
case 4:
|
||||
return const PetugasPenyewaView();
|
||||
case 5:
|
||||
return _buildBumdesTab();
|
||||
default:
|
||||
return _buildDashboardTab();
|
||||
@ -100,6 +101,16 @@ class PetugasBumdesDashboardView
|
||||
_buildWelcomeCard(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Tenant Statistics Section
|
||||
_buildSectionHeader(
|
||||
'Statistik Penyewa',
|
||||
AppColorsPetugas.blueGrotto,
|
||||
Icons.people_outline,
|
||||
),
|
||||
_buildTenantStatistics(),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Detail Status Sewa Aset section with improved header
|
||||
_buildSectionHeader(
|
||||
'Detail Status Sewa Aset',
|
||||
@ -118,8 +129,6 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
_buildRevenueStatistics(),
|
||||
const SizedBox(height: 16),
|
||||
_buildRevenueSources(),
|
||||
const SizedBox(height: 16),
|
||||
_buildRevenueTrend(),
|
||||
|
||||
// Add some padding at the bottom for better scrolling
|
||||
@ -156,25 +165,51 @@ class PetugasBumdesDashboardView
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
Obx(() {
|
||||
final avatar = controller.avatarUrl.value;
|
||||
if (avatar.isNotEmpty) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.network(
|
||||
avatar,
|
||||
width: 48,
|
||||
height: 48,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) => Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@ -208,15 +243,17 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Obx(
|
||||
() => Text(
|
||||
controller.userEmail.value,
|
||||
Obx(() {
|
||||
final name = controller.userName.value;
|
||||
final email = controller.userEmail.value;
|
||||
return Text(
|
||||
name.isNotEmpty ? name : email,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -642,19 +679,24 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Obx(
|
||||
() => Text(
|
||||
controller.totalPendapatanBulanIni.value,
|
||||
Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final total = stats['totalThisMonth'] ?? 0.0;
|
||||
return Text(
|
||||
formatRupiah(total),
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.success,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 6),
|
||||
Obx(
|
||||
() => Row(
|
||||
Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final percent = stats['percentComparedLast'] ?? 0.0;
|
||||
final isPositive = percent >= 0;
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@ -663,7 +705,7 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
controller.isKenaikanPositif.value
|
||||
isPositive
|
||||
? AppColorsPetugas.success.withOpacity(
|
||||
0.1,
|
||||
)
|
||||
@ -676,23 +718,23 @@ class PetugasBumdesDashboardView
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
controller.isKenaikanPositif.value
|
||||
isPositive
|
||||
? Icons.arrow_upward
|
||||
: Icons.arrow_downward,
|
||||
size: 14,
|
||||
color:
|
||||
controller.isKenaikanPositif.value
|
||||
isPositive
|
||||
? AppColorsPetugas.success
|
||||
: AppColorsPetugas.error,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
controller.persentaseKenaikan.value,
|
||||
'${percent.toStringAsFixed(1)}%',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color:
|
||||
controller.isKenaikanPositif.value
|
||||
isPositive
|
||||
? AppColorsPetugas.success
|
||||
: AppColorsPetugas.error,
|
||||
),
|
||||
@ -709,8 +751,8 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -744,16 +786,29 @@ class PetugasBumdesDashboardView
|
||||
}
|
||||
|
||||
Widget _buildRevenueSummary() {
|
||||
return Row(
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildRevenueQuickInfo(
|
||||
'Pendapatan Sewa',
|
||||
controller.pendapatanSewa.value,
|
||||
Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final totalTunai = stats['totalTunai'] ?? 0.0;
|
||||
return _buildRevenueQuickInfo(
|
||||
'Tunai',
|
||||
formatRupiah(totalTunai),
|
||||
AppColorsPetugas.navyBlue,
|
||||
Icons.shopping_cart_outlined,
|
||||
),
|
||||
),
|
||||
Icons.payments,
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 12),
|
||||
Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final totalTransfer = stats['totalTransfer'] ?? 0.0;
|
||||
return _buildRevenueQuickInfo(
|
||||
'Transfer',
|
||||
formatRupiah(totalTransfer),
|
||||
AppColorsPetugas.blueGrotto,
|
||||
Icons.account_balance,
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -811,81 +866,6 @@ class PetugasBumdesDashboardView
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRevenueSources() {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: AppColorsPetugas.shadowColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Sumber Pendapatan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
// Revenue Donut Chart
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.navyBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Sewa Aset',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Obx(
|
||||
() => Text(
|
||||
controller.pendapatanSewa.value,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'100% dari total pendapatan',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRevenueTrend() {
|
||||
final months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun'];
|
||||
|
||||
@ -912,6 +892,9 @@ class PetugasBumdesDashboardView
|
||||
child: Obx(() {
|
||||
// Get the trend data from controller
|
||||
final List<double> trendData = controller.trendPendapatan;
|
||||
if (trendData.isEmpty) {
|
||||
return Center(child: Text('Tidak ada data'));
|
||||
}
|
||||
final double maxValue = trendData.reduce(
|
||||
(curr, next) => curr > next ? curr : next,
|
||||
);
|
||||
@ -925,28 +908,28 @@ class PetugasBumdesDashboardView
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${maxValue.toStringAsFixed(1)}M',
|
||||
formatRupiah(maxValue),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(maxValue * 0.75).toStringAsFixed(1)}M',
|
||||
formatRupiah(maxValue * 0.75),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(maxValue * 0.5).toStringAsFixed(1)}M',
|
||||
formatRupiah(maxValue * 0.5),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(maxValue * 0.25).toStringAsFixed(1)}M',
|
||||
formatRupiah(maxValue * 0.25),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
@ -1004,7 +987,13 @@ class PetugasBumdesDashboardView
|
||||
children: [
|
||||
Container(
|
||||
width: 35,
|
||||
height: 170 * percentage,
|
||||
height:
|
||||
percentage.isNaN || percentage <= 0
|
||||
? 10.0
|
||||
: (170 * percentage).clamp(
|
||||
10.0,
|
||||
170.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(
|
||||
@ -1271,6 +1260,288 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// New widget for tenant statistics
|
||||
Widget _buildTenantStatistics() {
|
||||
return Obx(() {
|
||||
if (controller.isPenyewaStatsLoading.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: AppColorsPetugas.shadowColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
// Use LayoutBuilder to make the grid responsive
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 3,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisSpacing: 14,
|
||||
mainAxisSpacing: 14,
|
||||
childAspectRatio: 0.75,
|
||||
children: [
|
||||
_buildTenantStatusItem(
|
||||
'Menunggu Verifikasi',
|
||||
controller.penyewaPendingCount.value.toString(),
|
||||
AppColorsPetugas.warning,
|
||||
Icons.pending_outlined,
|
||||
),
|
||||
_buildTenantStatusItem(
|
||||
'Aktif',
|
||||
controller.penyewaActiveCount.value.toString(),
|
||||
AppColorsPetugas.success,
|
||||
Icons.check_circle_outline,
|
||||
),
|
||||
_buildTenantStatusItem(
|
||||
'Ditangguhkan',
|
||||
controller.penyewaSuspendedCount.value.toString(),
|
||||
AppColorsPetugas.error,
|
||||
Icons.block_outlined,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Tenant distribution visualization
|
||||
_buildTenantDistributionBar(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildTenantStatusItem(
|
||||
String title,
|
||||
String value,
|
||||
Color color,
|
||||
IconData icon,
|
||||
) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withOpacity(0.15),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
border: Border.all(color: color.withOpacity(0.1), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: color, size: 24),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
height: 1.2,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTenantDistributionBar() {
|
||||
// Calculate the total count for all tenant statuses
|
||||
final total = controller.penyewaTotalCount.value;
|
||||
|
||||
// Calculate percentages for each status (avoid division by zero)
|
||||
final pendingPercent =
|
||||
total > 0 ? controller.penyewaPendingCount.value / total : 0.0;
|
||||
final activePercent =
|
||||
total > 0 ? controller.penyewaActiveCount.value / total : 0.0;
|
||||
final suspendedPercent =
|
||||
total > 0 ? controller.penyewaSuspendedCount.value / total : 0.0;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Distribusi Status Penyewa',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Only show distribution bar if there are any tenants
|
||||
if (total > 0)
|
||||
Stack(
|
||||
children: [
|
||||
// Background for the progress bar
|
||||
Container(
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
// Actual progress bar segments
|
||||
Row(
|
||||
children: [
|
||||
if (pendingPercent > 0)
|
||||
_buildProgressSegment(
|
||||
pendingPercent,
|
||||
AppColorsPetugas.warning,
|
||||
isFirst: true,
|
||||
),
|
||||
if (activePercent > 0)
|
||||
_buildProgressSegment(
|
||||
activePercent,
|
||||
AppColorsPetugas.success,
|
||||
),
|
||||
if (suspendedPercent > 0)
|
||||
_buildProgressSegment(
|
||||
suspendedPercent,
|
||||
AppColorsPetugas.error,
|
||||
isLast: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Container(
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Belum ada data penyewa',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Use row layout for legends
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
if (pendingPercent > 0 || total == 0)
|
||||
_buildStatusLegend(
|
||||
'Menunggu Verifikasi',
|
||||
AppColorsPetugas.warning,
|
||||
pendingPercent,
|
||||
),
|
||||
if (activePercent > 0 || total == 0)
|
||||
_buildStatusLegend(
|
||||
'Aktif',
|
||||
AppColorsPetugas.success,
|
||||
activePercent,
|
||||
),
|
||||
if (suspendedPercent > 0 || total == 0)
|
||||
_buildStatusLegend(
|
||||
'Ditangguhkan',
|
||||
AppColorsPetugas.error,
|
||||
suspendedPercent,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressSegment(
|
||||
double percentage,
|
||||
Color color, {
|
||||
bool isFirst = false,
|
||||
bool isLast = false,
|
||||
}) {
|
||||
final flex = (percentage * 100).round();
|
||||
if (flex <= 0) return const SizedBox.shrink();
|
||||
|
||||
return Flexible(
|
||||
flex: flex,
|
||||
child: Container(
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.horizontal(
|
||||
left: isFirst ? const Radius.circular(6) : Radius.zero,
|
||||
right: isLast ? const Radius.circular(6) : Radius.zero,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusLegend(String text, Color color, double percentage) {
|
||||
final count = (percentage * 100).round();
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'$text ${count > 0 ? '($count%)' : ''}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.black87,
|
||||
fontWeight: count > 20 ? FontWeight.w500 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Custom clipper for creating pie/donut chart segments
|
||||
|
||||
275
lib/app/modules/petugas_bumdes/views/petugas_laporan_view.dart
Normal file
@ -0,0 +1,275 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:printing/printing.dart';
|
||||
import 'package:pdf/pdf.dart';
|
||||
import '../controllers/petugas_laporan_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
|
||||
class PetugasLaporanView extends GetView<PetugasLaporanController> {
|
||||
const PetugasLaporanView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get dashboard controller for side navbar
|
||||
final dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Laporan Bulanan'),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Filter Section
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Filter Laporan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => DropdownButtonFormField<int>(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Bulan',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
value: controller.selectedMonth.value,
|
||||
items:
|
||||
controller.months.map<DropdownMenuItem<int>>((
|
||||
month,
|
||||
) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: month['value'] as int,
|
||||
child: Text(month['label'] as String),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: controller.onMonthChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => DropdownButtonFormField<int>(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Tahun',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
value: controller.selectedYear.value,
|
||||
items:
|
||||
controller.years.map<DropdownMenuItem<int>>((
|
||||
year,
|
||||
) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: year,
|
||||
child: Text(year.toString()),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: controller.onYearChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => ElevatedButton.icon(
|
||||
onPressed:
|
||||
controller.isLoading.value
|
||||
? null
|
||||
: controller.generateReport,
|
||||
icon:
|
||||
controller.isLoading.value
|
||||
? Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: const CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.refresh),
|
||||
label: Text(
|
||||
controller.isLoading.value
|
||||
? 'Memproses...'
|
||||
: 'Generate Laporan',
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
),
|
||||
disabledBackgroundColor: AppColorsPetugas
|
||||
.blueGrotto
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Report Preview Section
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Menghasilkan laporan...'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!controller.isPdfReady.value ||
|
||||
controller.pdfBytes.value == null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.description_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Belum ada laporan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Pilih bulan dan tahun lalu klik "Generate Laporan"',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Preview Laporan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: Card(
|
||||
elevation: 2,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: PdfPreview(
|
||||
build: (format) => controller.pdfBytes.value!,
|
||||
canChangeOrientation: false,
|
||||
canChangePageFormat: false,
|
||||
canDebug: false,
|
||||
allowPrinting: false,
|
||||
allowSharing: false,
|
||||
initialPageFormat: PdfPageFormat.a4,
|
||||
pdfFileName:
|
||||
'Laporan_${controller.reportData['period']?['monthName'] ?? ''}_${controller.reportData['period']?['year'] ?? ''}.pdf',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: controller.savePdf,
|
||||
icon: const Icon(Icons.save_alt),
|
||||
label: const Text('Simpan PDF'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: controller.printPdf,
|
||||
icon: const Icon(Icons.print),
|
||||
label: const Text('Cetak'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -567,7 +567,7 @@ class PetugasManajemenBumdesView
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur tambah rekening bank sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
@ -639,7 +639,7 @@ class PetugasManajemenBumdesView
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur edit rekening bank sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
@ -689,7 +689,7 @@ class PetugasManajemenBumdesView
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur hapus rekening bank sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
@ -765,7 +765,7 @@ class PetugasManajemenBumdesView
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur tambah mitra sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
@ -845,7 +845,7 @@ class PetugasManajemenBumdesView
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur edit mitra sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
@ -895,7 +895,7 @@ class PetugasManajemenBumdesView
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur hapus mitra sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_paket_controller.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import 'package:bumrent_app/app/modules/petugas_bumdes/controllers/petugas_paket_controller.dart';
|
||||
import 'package:bumrent_app/app/routes/app_pages.dart';
|
||||
import 'package:bumrent_app/app/data/models/paket_model.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
|
||||
class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
const PetugasPaketView({Key? key}) : super(key: key);
|
||||
@ -53,7 +55,17 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET),
|
||||
onPressed: () async {
|
||||
final result = await Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_PAKET,
|
||||
arguments: {'isEditing': false},
|
||||
);
|
||||
|
||||
// Refresh the package list if a package was added
|
||||
if (result == true) {
|
||||
controller.loadPaketData();
|
||||
}
|
||||
},
|
||||
label: Text(
|
||||
'Tambah Paket',
|
||||
style: TextStyle(
|
||||
@ -115,7 +127,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.filteredPaketList.isEmpty) {
|
||||
if (controller.filteredPackages.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@ -136,7 +148,17 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET),
|
||||
onPressed: () async {
|
||||
final result = await Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_PAKET,
|
||||
arguments: {'isEditing': false},
|
||||
);
|
||||
|
||||
// Refresh the package list if a package was added
|
||||
if (result == true) {
|
||||
controller.loadPaketData();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Tambah Paket'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
@ -161,18 +183,192 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredPaketList.length,
|
||||
itemCount: controller.filteredPackages.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
final paket = controller.filteredPaketList[index];
|
||||
return _buildPaketCard(context, paket);
|
||||
if (index < controller.filteredPackages.length) {
|
||||
final paket = controller.filteredPackages[index];
|
||||
return _buildPaketCard(context, paket);
|
||||
} else {
|
||||
// Blank space at the end
|
||||
return const SizedBox(height: 80);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPaketCard(BuildContext context, Map<String, dynamic> paket) {
|
||||
final isAvailable = paket['tersedia'] == true;
|
||||
// Format price helper method
|
||||
String _formatPrice(dynamic price) {
|
||||
if (price == null) return '0';
|
||||
// If price is a string that can be parsed to a number
|
||||
if (price is String) {
|
||||
final number = double.tryParse(price) ?? 0;
|
||||
return number
|
||||
.toStringAsFixed(0)
|
||||
.replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]}.',
|
||||
);
|
||||
}
|
||||
// If price is already a number
|
||||
if (price is num) {
|
||||
return price
|
||||
.toStringAsFixed(0)
|
||||
.replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]}.',
|
||||
);
|
||||
}
|
||||
return '0';
|
||||
}
|
||||
|
||||
// Helper method to get time unit name based on ID
|
||||
String _getTimeUnitName(dynamic unitId) {
|
||||
if (unitId == null) return 'unit';
|
||||
|
||||
// Convert to string in case it's not already
|
||||
final unitIdStr = unitId.toString().toLowerCase();
|
||||
|
||||
// Map of known time unit IDs to their display names
|
||||
final timeUnitMap = {
|
||||
'6eaa32d9-855d-4214-b5b5-5c73d3edd9c5': 'jam',
|
||||
'582b7e66-6869-4495-9856-cef4a46683b0': 'hari',
|
||||
// Add more mappings as needed
|
||||
};
|
||||
|
||||
// If the unitId is a known ID, return the corresponding name
|
||||
if (timeUnitMap.containsKey(unitIdStr)) {
|
||||
return timeUnitMap[unitIdStr]!;
|
||||
}
|
||||
|
||||
// Check if the unit is already a name (like 'jam' or 'hari')
|
||||
final knownUnits = ['jam', 'hari', 'minggu', 'bulan'];
|
||||
if (knownUnits.contains(unitIdStr)) {
|
||||
return unitIdStr;
|
||||
}
|
||||
|
||||
// If the unit is a Map, try to extract the name from common fields
|
||||
if (unitId is Map) {
|
||||
return unitId['nama']?.toString().toLowerCase() ??
|
||||
unitId['name']?.toString().toLowerCase() ??
|
||||
unitId['satuan_waktu']?.toString().toLowerCase() ??
|
||||
'unit';
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 'unit';
|
||||
}
|
||||
|
||||
// Helper method to log time unit details
|
||||
void _logTimeUnitDetails(
|
||||
String packageName,
|
||||
List<Map<String, dynamic>> timeUnits,
|
||||
) {
|
||||
debugPrint('\n📦 [DEBUG] Package: $packageName');
|
||||
debugPrint('🔄 Found ${timeUnits.length} time units:');
|
||||
|
||||
for (var i = 0; i < timeUnits.length; i++) {
|
||||
final unit = timeUnits[i];
|
||||
debugPrint('\n ⏱️ Time Unit #${i + 1}:');
|
||||
|
||||
// Log all available keys and values
|
||||
debugPrint(' ├─ All fields: $unit');
|
||||
|
||||
// Log specific fields we're interested in
|
||||
unit.forEach((key, value) {
|
||||
debugPrint(' ├─ $key: $value (${value.runtimeType})');
|
||||
});
|
||||
|
||||
// Special handling for satuan_waktu if it's a map
|
||||
if (unit['satuan_waktu'] is Map) {
|
||||
final satuanWaktu = unit['satuan_waktu'] as Map;
|
||||
debugPrint(' └─ satuan_waktu details:');
|
||||
satuanWaktu.forEach((k, v) {
|
||||
debugPrint(' ├─ $k: $v (${v.runtimeType})');
|
||||
});
|
||||
}
|
||||
}
|
||||
debugPrint('\n');
|
||||
}
|
||||
|
||||
Widget _buildPaketCard(BuildContext context, dynamic paket) {
|
||||
// Handle both Map and PaketModel for backward compatibility
|
||||
final isPaketModel = paket is PaketModel;
|
||||
|
||||
debugPrint('\n🔍 [_buildPaketCard] Paket type: ${paket.runtimeType}');
|
||||
debugPrint('📋 Paket data: $paket');
|
||||
|
||||
// Extract status based on type
|
||||
final String status =
|
||||
isPaketModel
|
||||
? (paket.status?.toString().capitalizeFirst ?? 'Tidak Diketahui')
|
||||
: (paket['status']?.toString().capitalizeFirst ??
|
||||
'Tidak Diketahui');
|
||||
|
||||
debugPrint('🏷️ Extracted status: $status (isPaketModel: $isPaketModel)');
|
||||
|
||||
// Extract availability based on type
|
||||
final bool isAvailable =
|
||||
isPaketModel
|
||||
? (paket.kuantitas > 0)
|
||||
: ((paket['kuantitas'] as int?) ?? 0) > 0;
|
||||
|
||||
final String nama =
|
||||
isPaketModel
|
||||
? paket.nama
|
||||
: (paket['nama']?.toString() ?? 'Paket Tanpa Nama');
|
||||
|
||||
// Debug package info
|
||||
debugPrint('\n📦 [PACKAGE] ${paket.runtimeType} - $nama');
|
||||
debugPrint('├─ isPaketModel: $isPaketModel');
|
||||
debugPrint('├─ Available: $isAvailable');
|
||||
|
||||
// Get the first rental time unit price if available, otherwise use the base price
|
||||
final dynamic harga;
|
||||
if (isPaketModel) {
|
||||
if (paket.satuanWaktuSewa.isNotEmpty) {
|
||||
_logTimeUnitDetails(nama, paket.satuanWaktuSewa);
|
||||
|
||||
// Get the first time unit with its price
|
||||
final firstUnit = paket.satuanWaktuSewa.first;
|
||||
final firstUnitPrice = firstUnit['harga'];
|
||||
|
||||
debugPrint('💰 First time unit price: $firstUnitPrice');
|
||||
debugPrint('⏱️ First time unit ID: ${firstUnit['satuan_waktu_id']}');
|
||||
debugPrint('📝 First time unit details: $firstUnit');
|
||||
|
||||
// Always use the first time unit's price if available
|
||||
harga = firstUnitPrice ?? 0;
|
||||
} else {
|
||||
debugPrint('⚠️ No time units found for package: $nama');
|
||||
debugPrint('ℹ️ Using base price: ${paket.harga}');
|
||||
harga = paket.harga;
|
||||
}
|
||||
} else {
|
||||
// For non-PaketModel (Map) data
|
||||
if (isPaketModel && paket.satuanWaktuSewa.isNotEmpty) {
|
||||
final firstUnit = paket.satuanWaktuSewa.first;
|
||||
final firstUnitPrice = firstUnit['harga'];
|
||||
debugPrint('💰 [MAP] First time unit price: $firstUnitPrice');
|
||||
harga = firstUnitPrice ?? 0;
|
||||
} else {
|
||||
debugPrint('⚠️ [MAP] No time units found for package: $nama');
|
||||
debugPrint('ℹ️ [MAP] Using base price: ${paket['harga']}');
|
||||
harga = paket['harga'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('💵 Final price being used: $harga\n');
|
||||
|
||||
// Get the main photo URL
|
||||
final String? foto =
|
||||
isPaketModel
|
||||
? (paket.images?.isNotEmpty == true
|
||||
? paket.images!.first
|
||||
: paket.foto_paket)
|
||||
: (paket['foto_paket']?.toString() ??
|
||||
(paket['foto'] is String ? paket['foto'] : null));
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
@ -192,26 +388,101 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _showPaketDetails(context, paket),
|
||||
onTap: () async {
|
||||
final result = await Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_PAKET,
|
||||
arguments: {
|
||||
'isEditing': false,
|
||||
'isViewing': true,
|
||||
'paket': paket,
|
||||
},
|
||||
);
|
||||
|
||||
// Refresh the package list if data was modified
|
||||
if (result == true) {
|
||||
controller.loadPaketData();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
// Paket image or icon
|
||||
Container(
|
||||
SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
bottomLeft: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getPaketIcon(paket['kategori']),
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
size: 32,
|
||||
),
|
||||
child:
|
||||
foto != null && foto.isNotEmpty
|
||||
? Image.network(
|
||||
foto,
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) => Container(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getPaketIcon(
|
||||
_getTimeUnitName(
|
||||
isPaketModel
|
||||
? (paket
|
||||
.satuanWaktuSewa
|
||||
.isNotEmpty
|
||||
? paket
|
||||
.satuanWaktuSewa
|
||||
.first['satuan_waktu_id'] ??
|
||||
'hari'
|
||||
: 'hari')
|
||||
: (paket['satuanWaktuSewa'] !=
|
||||
null &&
|
||||
paket['satuanWaktuSewa']
|
||||
.isNotEmpty
|
||||
? paket['satuanWaktuSewa'][0]['satuan_waktu_id']
|
||||
?.toString() ??
|
||||
'hari'
|
||||
: 'hari'),
|
||||
),
|
||||
),
|
||||
color: AppColorsPetugas.navyBlue
|
||||
.withOpacity(0.5),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getPaketIcon(
|
||||
_getTimeUnitName(
|
||||
isPaketModel
|
||||
? (paket.satuanWaktuSewa.isNotEmpty
|
||||
? paket
|
||||
.satuanWaktuSewa
|
||||
.first['satuan_waktu_id'] ??
|
||||
'hari'
|
||||
: 'hari')
|
||||
: (paket['satuanWaktuSewa'] != null &&
|
||||
paket['satuanWaktuSewa']
|
||||
.isNotEmpty
|
||||
? paket['satuanWaktuSewa'][0]['satuan_waktu_id']
|
||||
?.toString() ??
|
||||
'hari'
|
||||
: 'hari'),
|
||||
),
|
||||
),
|
||||
color: AppColorsPetugas.navyBlue.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -228,9 +499,10 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Package name
|
||||
Text(
|
||||
paket['nama'],
|
||||
style: TextStyle(
|
||||
nama,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
@ -239,13 +511,119 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Rp ${_formatPrice(paket['harga'])}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
// Prices with time units
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final List<Map<String, dynamic>> timeUnits =
|
||||
[];
|
||||
|
||||
// Get all time units
|
||||
if (isPaketModel &&
|
||||
paket.satuanWaktuSewa.isNotEmpty) {
|
||||
timeUnits.addAll(paket.satuanWaktuSewa);
|
||||
} else if (!isPaketModel &&
|
||||
paket['satuanWaktuSewa'] != null &&
|
||||
paket['satuanWaktuSewa'].isNotEmpty) {
|
||||
timeUnits.addAll(
|
||||
List<Map<String, dynamic>>.from(
|
||||
paket['satuanWaktuSewa'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// If no time units, show nothing
|
||||
if (timeUnits.isEmpty)
|
||||
return const SizedBox.shrink();
|
||||
|
||||
// Filter out time units with price 0 or null
|
||||
final validTimeUnits =
|
||||
timeUnits.where((unit) {
|
||||
final price =
|
||||
unit['harga'] is int
|
||||
? unit['harga']
|
||||
: int.tryParse(
|
||||
unit['harga']
|
||||
?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0;
|
||||
return price > 0;
|
||||
}).toList();
|
||||
|
||||
if (validTimeUnits.isEmpty)
|
||||
return const SizedBox.shrink();
|
||||
|
||||
return Column(
|
||||
children:
|
||||
validTimeUnits
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) {
|
||||
final index = entry.key;
|
||||
final unit = entry.value;
|
||||
final unitPrice =
|
||||
unit['harga'] is int
|
||||
? unit['harga']
|
||||
: int.tryParse(
|
||||
unit['harga']
|
||||
?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0;
|
||||
final unitName = _getTimeUnitName(
|
||||
unit['satuan_waktu_id'],
|
||||
);
|
||||
final isFirst = index == 0;
|
||||
|
||||
if (unitPrice <= 0)
|
||||
return const SizedBox.shrink();
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Rp ${_formatPrice(unitPrice)}/$unitName',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
AppColorsPetugas
|
||||
.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
})
|
||||
.where(
|
||||
(widget) => widget is! SizedBox,
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!isPaketModel &&
|
||||
paket['harga'] != null &&
|
||||
(paket['harga'] is int
|
||||
? paket['harga']
|
||||
: int.tryParse(
|
||||
paket['harga']?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0) >
|
||||
0) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Rp ${_formatPrice(paket['harga'])}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -258,25 +636,31 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isAvailable
|
||||
status.toLowerCase() == 'tersedia'
|
||||
? AppColorsPetugas.successLight
|
||||
: status.toLowerCase() == 'pemeliharaan'
|
||||
? AppColorsPetugas.warningLight
|
||||
: AppColorsPetugas.errorLight,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color:
|
||||
isAvailable
|
||||
status.toLowerCase() == 'tersedia'
|
||||
? AppColorsPetugas.success
|
||||
: status.toLowerCase() == 'pemeliharaan'
|
||||
? AppColorsPetugas.warning
|
||||
: AppColorsPetugas.error,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
isAvailable ? 'Aktif' : 'Nonaktif',
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color:
|
||||
isAvailable
|
||||
status.toLowerCase() == 'tersedia'
|
||||
? AppColorsPetugas.success
|
||||
: status.toLowerCase() == 'pemeliharaan'
|
||||
? AppColorsPetugas.warning
|
||||
: AppColorsPetugas.error,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
@ -290,9 +674,12 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
// Edit icon
|
||||
GestureDetector(
|
||||
onTap:
|
||||
() => _showAddEditPaketDialog(
|
||||
context,
|
||||
paket: paket,
|
||||
() => Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_PAKET,
|
||||
arguments: {
|
||||
'isEditing': true,
|
||||
'paket': paket,
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
@ -350,33 +737,42 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPrice(dynamic price) {
|
||||
if (price == null) return '0';
|
||||
|
||||
// Convert the price to string and handle formatting
|
||||
String priceStr = price.toString();
|
||||
|
||||
// Add thousand separators
|
||||
final RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))');
|
||||
String formatted = priceStr.replaceAllMapped(reg, (Match m) => '${m[1]}.');
|
||||
|
||||
return formatted;
|
||||
// Add this helper method to get color based on status
|
||||
Color _getStatusColor(String status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'aktif':
|
||||
return AppColorsPetugas.success;
|
||||
case 'tidak aktif':
|
||||
case 'nonaktif':
|
||||
return AppColorsPetugas.error;
|
||||
case 'dalam perbaikan':
|
||||
case 'maintenance':
|
||||
return AppColorsPetugas.warning;
|
||||
case 'tersedia':
|
||||
return AppColorsPetugas.success;
|
||||
case 'pemeliharaan':
|
||||
return AppColorsPetugas.warning;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getPaketIcon(String? category) {
|
||||
if (category == null) return Icons.category;
|
||||
IconData _getPaketIcon(String? timeUnit) {
|
||||
if (timeUnit == null) return Icons.access_time;
|
||||
|
||||
switch (category.toLowerCase()) {
|
||||
case 'bulanan':
|
||||
return Icons.calendar_month;
|
||||
case 'tahunan':
|
||||
switch (timeUnit.toLowerCase()) {
|
||||
case 'jam':
|
||||
return Icons.access_time;
|
||||
case 'hari':
|
||||
return Icons.calendar_today;
|
||||
case 'premium':
|
||||
return Icons.star;
|
||||
case 'bisnis':
|
||||
return Icons.business;
|
||||
case 'minggu':
|
||||
return Icons.date_range;
|
||||
case 'bulan':
|
||||
return Icons.calendar_month;
|
||||
case 'tahun':
|
||||
return Icons.calendar_view_month;
|
||||
default:
|
||||
return Icons.category;
|
||||
return Icons.access_time;
|
||||
}
|
||||
}
|
||||
|
||||
@ -426,273 +822,104 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showPaketDetails(BuildContext context, Map<String, dynamic> paket) {
|
||||
showModalBottomSheet(
|
||||
void _showDeleteConfirmation(BuildContext context, dynamic paket) {
|
||||
// Handle both Map and PaketModel for backward compatibility
|
||||
final isPaketModel = paket is PaketModel;
|
||||
final String id = isPaketModel ? paket.id : (paket['id']?.toString() ?? '');
|
||||
final String nama =
|
||||
isPaketModel ? paket.nama : (paket['nama']?.toString() ?? 'Paket');
|
||||
showDialog(
|
||||
context: context,
|
||||
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,
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Warning icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.errorLight,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, color: AppColorsPetugas.blueGrotto),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Icon(
|
||||
Icons.delete_forever,
|
||||
color: AppColorsPetugas.error,
|
||||
size: 32,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Title and message
|
||||
Text(
|
||||
'Konfirmasi Hapus',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Text(
|
||||
'Apakah Anda yakin ingin menghapus paket "$nama"? Tindakan ini tidak dapat dibatalkan.',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
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']),
|
||||
],
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColorsPetugas.textPrimary,
|
||||
side: BorderSide(color: AppColorsPetugas.divider),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
),
|
||||
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(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
controller.deletePaket(id);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.error,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
777
lib/app/modules/petugas_bumdes/views/petugas_penyewa_view.dart
Normal file
@ -0,0 +1,777 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_penyewa_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class PetugasPenyewaView extends StatefulWidget {
|
||||
const PetugasPenyewaView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PetugasPenyewaView> createState() => _PetugasPenyewaViewState();
|
||||
}
|
||||
|
||||
class _PetugasPenyewaViewState extends State<PetugasPenyewaView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
late PetugasPenyewaController controller;
|
||||
late PetugasBumdesDashboardController dashboardController;
|
||||
|
||||
final List<String> tabTitles = ['Verifikasi', 'Aktif', 'Ditangguhkan'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = Get.find<PetugasPenyewaController>();
|
||||
dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
|
||||
// Add listener to sync tab selection with controller's filter
|
||||
_tabController.addListener(_onTabChanged);
|
||||
}
|
||||
|
||||
void _onTabChanged() {
|
||||
if (!_tabController.indexIsChanging) {
|
||||
controller.changeTab(_tabController.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(
|
||||
'Daftar Penyewa',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
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:
|
||||
tabTitles
|
||||
.map(
|
||||
(title) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Tab(text: title),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
dividerColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
// Tambahkan indikator refresh
|
||||
Obx(
|
||||
() =>
|
||||
controller.isRefreshing.value
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
drawer: PetugasSideNavbar(controller: dashboardController),
|
||||
drawerEdgeDragWidth: 60,
|
||||
drawerScrimColor: Colors.black.withOpacity(0.6),
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children:
|
||||
[0, 1, 2].map((index) {
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.filteredPenyewaList.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.refreshPenyewaList();
|
||||
},
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
child: _buildEmptyStateWithScrollView(),
|
||||
);
|
||||
}
|
||||
|
||||
return _buildPenyewaList();
|
||||
});
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: Obx(
|
||||
() => PetugasBumdesBottomNavbar(
|
||||
selectedIndex: dashboardController.currentTabIndex.value,
|
||||
onItemTapped: (index) => dashboardController.changeTab(index),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
// Add controller for TextField so it can be cleared
|
||||
final TextEditingController searchController = TextEditingController(
|
||||
text: controller.searchQuery.value,
|
||||
);
|
||||
|
||||
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(
|
||||
controller: searchController,
|
||||
onChanged: controller.updateSearchQuery,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari nama atau email...',
|
||||
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: Obx(
|
||||
() =>
|
||||
controller.searchQuery.value.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
controller.updateSearchQuery('');
|
||||
},
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.people_outline, size: 80, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Obx(() {
|
||||
String message = 'Belum ada data penyewa';
|
||||
|
||||
if (controller.searchQuery.isNotEmpty) {
|
||||
message = 'Tidak ada hasil yang cocok dengan pencarian';
|
||||
} else {
|
||||
switch (controller.currentTabIndex.value) {
|
||||
case 0:
|
||||
message = 'Tidak ada penyewa yang menunggu verifikasi';
|
||||
break;
|
||||
case 1:
|
||||
message = 'Tidak ada penyewa aktif';
|
||||
break;
|
||||
case 2:
|
||||
message = 'Tidak ada penyewa yang ditangguhkan';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Data penyewa akan muncul di sini',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk menampilkan empty state dengan ScrollView untuk mendukung RefreshIndicator
|
||||
Widget _buildEmptyStateWithScrollView() {
|
||||
return ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(Get.context!).size.height * 0.7,
|
||||
child: _buildEmptyState(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPenyewaList() {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.refreshPenyewaList();
|
||||
},
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredPenyewaList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final penyewa = controller.filteredPenyewaList[index];
|
||||
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, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header with avatar and badge
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight.withOpacity(
|
||||
0.2,
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Avatar with border
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||
backgroundImage:
|
||||
penyewa['avatar'] != null &&
|
||||
penyewa['avatar']
|
||||
.toString()
|
||||
.isNotEmpty
|
||||
? NetworkImage(penyewa['avatar'])
|
||||
: null,
|
||||
child:
|
||||
penyewa['avatar'] == null ||
|
||||
penyewa['avatar'].toString().isEmpty
|
||||
? const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Name and email
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
penyewa['nama_lengkap'] ??
|
||||
'Nama tidak tersedia',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.email_outlined,
|
||||
size: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
penyewa['email'] ??
|
||||
'Email tidak tersedia',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildStatusBadge(penyewa['status']),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Content section
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Show additional info only for Aktif and Ditangguhkan tabs
|
||||
if (controller.currentTabIndex.value != 0) ...[
|
||||
Row(
|
||||
children: [
|
||||
_buildInfoChip(
|
||||
Icons.credit_card_outlined,
|
||||
'NIK: ${penyewa['nik'] ?? 'Tidak tersedia'}',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildInfoChip(
|
||||
Icons.phone_outlined,
|
||||
penyewa['no_hp'] ?? 'No. HP tidak tersedia',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildInfoTile(
|
||||
'Total Sewa',
|
||||
penyewa['total_sewa']?.toString() ?? '0',
|
||||
Icons.shopping_bag_outlined,
|
||||
AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
if (controller.currentTabIndex.value == 1 ||
|
||||
controller.currentTabIndex.value == 2)
|
||||
_buildActionChip(
|
||||
label: 'Lihat Detail',
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
icon: Icons.visibility,
|
||||
onTap: () {
|
||||
Get.toNamed(
|
||||
Routes.PETUGAS_DETAIL_PENYEWA,
|
||||
arguments: {
|
||||
'userId': penyewa['user_id'],
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
// Add "Detail" button for Verifikasi tab
|
||||
if (controller.currentTabIndex.value == 0) ...[
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildActionChip(
|
||||
label: 'Lihat Detail & Verifikasi',
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
icon: Icons.visibility,
|
||||
onTap: () {
|
||||
Get.toNamed(
|
||||
Routes.PETUGAS_DETAIL_PENYEWA,
|
||||
arguments: {
|
||||
'userId': penyewa['user_id'],
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoChip(IconData icon, String label) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: Colors.grey[600]),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[700],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionChip({
|
||||
required String label,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
IconData? icon,
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withOpacity(0.5)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: 14, color: color),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoTile(
|
||||
String label,
|
||||
String value,
|
||||
IconData icon,
|
||||
Color color,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, size: 14, color: color),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusBadge(String? status) {
|
||||
Color bgColor;
|
||||
Color textColor;
|
||||
String label;
|
||||
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'active':
|
||||
bgColor = Colors.green[100]!;
|
||||
textColor = Colors.green[800]!;
|
||||
label = 'Aktif';
|
||||
break;
|
||||
case 'pending':
|
||||
bgColor = Colors.orange[100]!;
|
||||
textColor = Colors.orange[800]!;
|
||||
label = 'Menunggu';
|
||||
break;
|
||||
case 'suspended':
|
||||
bgColor = Colors.red[100]!;
|
||||
textColor = Colors.red[800]!;
|
||||
label = 'Dinonaktifkan';
|
||||
break;
|
||||
default:
|
||||
bgColor = Colors.grey[100]!;
|
||||
textColor = Colors.grey[800]!;
|
||||
label = 'Tidak diketahui';
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showApproveDialog(BuildContext context, String userId) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('Konfirmasi Aktivasi'),
|
||||
content: const Text(
|
||||
'Apakah Anda yakin ingin mengaktifkan penyewa ini?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
controller.updatePenyewaStatus(
|
||||
userId,
|
||||
'active',
|
||||
'Akun diaktifkan oleh petugas',
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Aktifkan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showRejectDialog(BuildContext context, String userId) {
|
||||
final reasonController = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('Konfirmasi Penolakan'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Apakah Anda yakin ingin menolak penyewa ini?'),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: reasonController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Alasan Penolakan',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
final reason =
|
||||
reasonController.text.isNotEmpty
|
||||
? reasonController.text
|
||||
: 'Ditolak oleh petugas';
|
||||
controller.updatePenyewaStatus(userId, 'suspended', reason);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Tolak'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import 'petugas_detail_sewa_view.dart';
|
||||
import '../../../data/models/rental_booking_model.dart';
|
||||
|
||||
class PetugasSewaView extends StatefulWidget {
|
||||
const PetugasSewaView({Key? key}) : super(key: key);
|
||||
@ -160,6 +161,10 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
}
|
||||
|
||||
Widget _buildSearchSection() {
|
||||
// Tambahkan controller untuk TextField agar bisa dikosongkan
|
||||
final TextEditingController searchController = TextEditingController(
|
||||
text: controller.searchQuery.value,
|
||||
);
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
@ -173,9 +178,9 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
onChanged: (value) {
|
||||
controller.setSearchQuery(value);
|
||||
controller.setOrderIdQuery(value);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari nama warga atau ID pesanan...',
|
||||
@ -204,10 +209,21 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true,
|
||||
suffixIcon: Icon(
|
||||
Icons.tune_rounded,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
size: 20,
|
||||
suffixIcon: Obx(
|
||||
() =>
|
||||
controller.searchQuery.value.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
controller.setSearchQuery('');
|
||||
},
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -241,17 +257,44 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
final filteredList =
|
||||
status == 'Semua'
|
||||
? controller.filteredSewaList
|
||||
: status == 'Menunggu Pembayaran'
|
||||
? controller.sewaList
|
||||
.where(
|
||||
(sewa) =>
|
||||
sewa.status.toUpperCase() == 'MENUNGGU PEMBAYARAN' ||
|
||||
sewa.status.toUpperCase() == 'PEMBAYARAN DENDA',
|
||||
)
|
||||
.toList()
|
||||
: status == 'Periksa Pembayaran'
|
||||
? controller.sewaList
|
||||
.where(
|
||||
(sewa) =>
|
||||
sewa['status'] == 'Periksa Pembayaran' ||
|
||||
sewa['status'] == 'Pembayaran Denda' ||
|
||||
sewa['status'] == 'Periksa Denda',
|
||||
sewa.status.toUpperCase() == 'PERIKSA PEMBAYARAN' ||
|
||||
sewa.status.toUpperCase() == 'PERIKSA PEMBAYARAN DENDA',
|
||||
)
|
||||
.toList()
|
||||
: status == 'Diterima'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'DITERIMA')
|
||||
.toList()
|
||||
: status == 'Aktif'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'AKTIF')
|
||||
.toList()
|
||||
: status == 'Dikembalikan'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'DIKEMBALIKAN')
|
||||
.toList()
|
||||
: status == 'Selesai'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'SELESAI')
|
||||
.toList()
|
||||
: status == 'Dibatalkan'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'DIBATALKAN')
|
||||
.toList()
|
||||
: controller.sewaList
|
||||
.where((sewa) => sewa['status'] == status)
|
||||
.where((sewa) => sewa.status == status)
|
||||
.toList();
|
||||
|
||||
if (filteredList.isEmpty) {
|
||||
@ -313,40 +356,25 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildSewaCard(BuildContext context, Map<String, dynamic> sewa) {
|
||||
final statusColor = controller.getStatusColor(sewa['status']);
|
||||
final status = sewa['status'];
|
||||
Widget _buildSewaCard(BuildContext context, SewaModel sewa) {
|
||||
final statusColor = controller.getStatusColor(sewa.status);
|
||||
final status = sewa.status;
|
||||
|
||||
// Get appropriate icon for status
|
||||
IconData statusIcon;
|
||||
switch (status) {
|
||||
case 'Menunggu Pembayaran':
|
||||
statusIcon = Icons.payments_outlined;
|
||||
break;
|
||||
case 'Periksa Pembayaran':
|
||||
statusIcon = Icons.fact_check_outlined;
|
||||
break;
|
||||
case 'Diterima':
|
||||
statusIcon = Icons.check_circle_outlined;
|
||||
break;
|
||||
case 'Pembayaran Denda':
|
||||
statusIcon = Icons.money_off_csred_outlined;
|
||||
break;
|
||||
case 'Periksa Denda':
|
||||
statusIcon = Icons.assignment_late_outlined;
|
||||
break;
|
||||
case 'Dikembalikan':
|
||||
statusIcon = Icons.assignment_return_outlined;
|
||||
break;
|
||||
case 'Selesai':
|
||||
statusIcon = Icons.task_alt_outlined;
|
||||
break;
|
||||
case 'Dibatalkan':
|
||||
statusIcon = Icons.cancel_outlined;
|
||||
break;
|
||||
default:
|
||||
statusIcon = Icons.help_outline_rounded;
|
||||
}
|
||||
IconData statusIcon = controller.getStatusIcon(status);
|
||||
|
||||
// Flag untuk membedakan tipe pesanan
|
||||
final bool isAset = sewa.tipePesanan == 'tunggal';
|
||||
final bool isPaket = sewa.tipePesanan == 'paket';
|
||||
|
||||
// Pilih nama aset/paket
|
||||
final String namaAsetAtauPaket =
|
||||
isAset
|
||||
? (sewa.asetNama ?? '-')
|
||||
: (isPaket ? (sewa.paketNama ?? '-') : '-');
|
||||
// Pilih foto aset/paket jika ingin digunakan
|
||||
final String? fotoAsetAtauPaket =
|
||||
isAset ? sewa.asetFoto : (isPaket ? sewa.paketFoto : null);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
@ -370,6 +398,35 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Status header inside the card
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.12),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(statusIcon, size: 16, color: statusColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
|
||||
child: Row(
|
||||
@ -378,14 +435,22 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||
child: Text(
|
||||
sewa['nama_warga'].substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
backgroundImage:
|
||||
(sewa.wargaAvatar != null &&
|
||||
sewa.wargaAvatar.isNotEmpty)
|
||||
? NetworkImage(sewa.wargaAvatar)
|
||||
: null,
|
||||
child:
|
||||
(sewa.wargaAvatar == null || sewa.wargaAvatar.isEmpty)
|
||||
? Text(
|
||||
sewa.wargaNama.substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
@ -395,79 +460,47 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sewa['nama_warga'],
|
||||
sewa.wargaNama,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
statusIcon,
|
||||
size: 12,
|
||||
color: statusColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'#${sewa['order_id']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
'Tanggal Pesan: ' +
|
||||
(sewa.tanggalPemesanan != null
|
||||
? '${sewa.tanggalPemesanan.day.toString().padLeft(2, '0')}-${sewa.tanggalPemesanan.month.toString().padLeft(2, '0')}-${sewa.tanggalPemesanan.year}'
|
||||
: '-'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 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,
|
||||
// Price - only show if total_tagihan > 0
|
||||
if (sewa.totalTagihan > 0)
|
||||
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.totalTagihan),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -481,33 +514,51 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
child: Divider(height: 1, color: Colors.grey.shade200),
|
||||
),
|
||||
|
||||
// Asset details
|
||||
// Asset/Paket details
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
// Asset icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
// Asset/Paket image or icon
|
||||
if (fotoAsetAtauPaket != null &&
|
||||
fotoAsetAtauPaket.isNotEmpty)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Image.network(
|
||||
fotoAsetAtauPaket,
|
||||
width: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) => Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 28,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 20,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 20,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Asset name and duration
|
||||
// Asset/Paket name and duration
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sewa['nama_aset'],
|
||||
namaAsetAtauPaket,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
@ -524,7 +575,7 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${sewa['tanggal_mulai']} - ${sewa['tanggal_selesai']}',
|
||||
_formatDateRange(sewa),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
@ -552,6 +603,37 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDateRange(SewaModel sewa) {
|
||||
final startDate = sewa.waktuMulai;
|
||||
final endDate = sewa.waktuSelesai;
|
||||
|
||||
// Format dates as dd-mm-yyyy
|
||||
String formattedStartDate =
|
||||
'${startDate.day.toString().padLeft(2, '0')}-${startDate.month.toString().padLeft(2, '0')}-${startDate.year}';
|
||||
String formattedEndDate =
|
||||
'${endDate.day.toString().padLeft(2, '0')}-${endDate.month.toString().padLeft(2, '0')}-${endDate.year}';
|
||||
|
||||
// Check if rental unit is "jam" (hour)
|
||||
if (sewa.namaSatuanWaktu?.toLowerCase() == 'jam') {
|
||||
// Format as "dd-mm-yyyy icon jam 09.00-15.00"
|
||||
String startTime =
|
||||
'${startDate.hour.toString().padLeft(2, '0')}.${startDate.minute.toString().padLeft(2, '0')}';
|
||||
String endTime =
|
||||
'${endDate.hour.toString().padLeft(2, '0')}.${endDate.minute.toString().padLeft(2, '0')}';
|
||||
return '$formattedStartDate ⏱ $startTime-$endTime';
|
||||
}
|
||||
// If same day but not hourly, just show the date
|
||||
else if (startDate.day == endDate.day &&
|
||||
startDate.month == endDate.month &&
|
||||
startDate.year == endDate.year) {
|
||||
return formattedStartDate;
|
||||
}
|
||||
// Different days - show date range
|
||||
else {
|
||||
return '$formattedStartDate - $formattedEndDate';
|
||||
}
|
||||
}
|
||||
|
||||
void _showFilterBottomSheet() {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
@ -9,32 +10,51 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Tambah Aset',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [_buildHeaderSection(), _buildFormSection(context)],
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Obx(() => Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
controller.isEditing.value ? 'Edit Aset' : 'Tambah Aset',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: _buildBottomBar(),
|
||||
body: Stack(
|
||||
children: [
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeaderSection(),
|
||||
_buildFormSection(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.isLoading.value)
|
||||
Container(
|
||||
color: Colors.black54,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColorsPetugas.blueGrotto),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: _buildBottomBar(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.only(top: 10, left: 20, right: 20, bottom: 5), // Reduced padding
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto],
|
||||
@ -42,50 +62,8 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Informasi Aset Baru',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Isi data dengan lengkap untuk menambahkan aset',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: Container(
|
||||
height: 12, // Further reduced height
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -131,69 +109,36 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
_buildImageUploader(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Category Section
|
||||
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'),
|
||||
// Status Section
|
||||
_buildSectionHeader(icon: Icons.check_circle, title: 'Status'),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Category and Status as cards
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Kategori',
|
||||
options: controller.categoryOptions,
|
||||
selectedOption: controller.selectedCategory,
|
||||
onChanged: controller.setCategory,
|
||||
icon: Icons.inventory_2,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Status',
|
||||
options: controller.statusOptions,
|
||||
selectedOption: controller.selectedStatus,
|
||||
onChanged: controller.setStatus,
|
||||
icon: Icons.check_circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
// Status card
|
||||
_buildCategorySelect(
|
||||
title: 'Status',
|
||||
options: controller.statusOptions,
|
||||
selectedOption: controller.selectedStatus,
|
||||
onChanged: controller.setStatus,
|
||||
icon: Icons.check_circle,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Quantity Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.format_list_numbered,
|
||||
title: 'Kuantitas & Pengukuran',
|
||||
title: 'Kuantitas',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quantity fields
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildTextField(
|
||||
label: 'Kuantitas',
|
||||
hint: 'Jumlah aset',
|
||||
controller: controller.quantityController,
|
||||
isRequired: true,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
prefixIcon: Icons.numbers,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: _buildTextField(
|
||||
label: 'Satuan Ukur',
|
||||
hint: 'contoh: Unit, Buah',
|
||||
controller: controller.unitOfMeasureController,
|
||||
prefixIcon: Icons.straighten,
|
||||
),
|
||||
),
|
||||
],
|
||||
// Quantity field
|
||||
_buildTextField(
|
||||
label: 'Kuantitas',
|
||||
hint: 'Jumlah aset',
|
||||
controller: controller.quantityController,
|
||||
isRequired: true,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
prefixIcon: Icons.numbers,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@ -654,6 +599,114 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
);
|
||||
}
|
||||
|
||||
// Show image source options
|
||||
void _showImageSourceOptions() {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Pilih Sumber Gambar',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildImageSourceOption(
|
||||
icon: Icons.camera_alt,
|
||||
label: 'Kamera',
|
||||
onTap: () {
|
||||
Get.back();
|
||||
controller.pickImageFromCamera();
|
||||
},
|
||||
),
|
||||
_buildImageSourceOption(
|
||||
icon: Icons.photo_library,
|
||||
label: 'Galeri',
|
||||
onTap: () {
|
||||
Get.back();
|
||||
controller.pickImageFromGallery();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageSourceOption({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 30,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageUploader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@ -696,7 +749,7 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
children: [
|
||||
// Add button
|
||||
GestureDetector(
|
||||
onTap: () => controller.addSampleImage(),
|
||||
onTap: _showImageSourceOptions,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
@ -732,69 +785,107 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
),
|
||||
|
||||
// Image previews
|
||||
...controller.selectedImages.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
...List<Widget>.generate(
|
||||
controller.selectedImages.length,
|
||||
(index) => Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: Obx(
|
||||
() {
|
||||
// Check if we have a network URL for this index
|
||||
if (index < controller.networkImageUrls.length &&
|
||||
controller.networkImageUrls[index].isNotEmpty) {
|
||||
// Display network image
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
controller.networkImageUrls[index],
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Center(
|
||||
child: Icon(Icons.error_outline, color: Colors.red),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Display local file
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: FutureBuilder<File>(
|
||||
future: File(controller.selectedImages[index].path).exists().then((exists) {
|
||||
if (exists) {
|
||||
return File(controller.selectedImages[index].path);
|
||||
} else {
|
||||
return File(controller.selectedImages[index].path);
|
||||
}
|
||||
}),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data != null) {
|
||||
return Image.file(
|
||||
snapshot.data!,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
color: Colors.grey[200],
|
||||
child: const Icon(Icons.broken_image, color: Colors.grey),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
color: Colors.grey[200],
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.removeImage(index),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 40,
|
||||
),
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.removeImage(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: AppColorsPetugas.error,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -850,7 +941,9 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.save),
|
||||
label: Text(isSubmitting ? 'Menyimpan...' : 'Simpan Aset'),
|
||||
label: Obx(() => Text(
|
||||
isSubmitting ? 'Menyimpan...' : (controller.isEditing.value ? 'Simpan Perubahan' : 'Simpan Aset'),
|
||||
)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
|
||||
@ -64,6 +64,14 @@ class PetugasBumdesBottomNavbar extends StatelessWidget {
|
||||
isSelected: selectedIndex == 3,
|
||||
onTap: () => onItemTapped(3),
|
||||
),
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
icon: Icons.people_outlined,
|
||||
activeIcon: Icons.people,
|
||||
label: 'Penyewa',
|
||||
isSelected: selectedIndex == 4,
|
||||
onTap: () => onItemTapped(4),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -79,7 +87,7 @@ class PetugasBumdesBottomNavbar extends StatelessWidget {
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final primaryColor = AppColors.primary;
|
||||
final tabWidth = MediaQuery.of(context).size.width / 4;
|
||||
final tabWidth = MediaQuery.of(context).size.width / 5;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class PetugasSideNavbar extends StatelessWidget {
|
||||
final PetugasBumdesDashboardController controller;
|
||||
@ -11,7 +13,7 @@ class PetugasSideNavbar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
@ -32,24 +34,46 @@ class PetugasSideNavbar extends StatelessWidget {
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
|
||||
color: AppColors.primary,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.white,
|
||||
child: Icon(Icons.person, color: AppColors.primary, size: 36),
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
final avatar = controller.avatarUrl.value;
|
||||
if (avatar.isNotEmpty) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundImage: NetworkImage(avatar),
|
||||
onBackgroundImageError: (error, stackTrace) {},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.white,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: AppColors.primary,
|
||||
size: 36,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@ -125,6 +149,30 @@ class PetugasSideNavbar extends StatelessWidget {
|
||||
isSelected: controller.currentTabIndex.value == 3,
|
||||
onTap: () => controller.changeTab(3),
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.people_outlined,
|
||||
activeIcon: Icons.people,
|
||||
title: 'Penyewa',
|
||||
subtitle: 'Kelola data penyewa',
|
||||
isSelected: controller.currentTabIndex.value == 4,
|
||||
onTap: () => controller.changeTab(4),
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.account_balance_outlined,
|
||||
activeIcon: Icons.account_balance,
|
||||
title: 'Kelola Akun Bank',
|
||||
subtitle: 'Kelola akun bank BUMDes',
|
||||
isSelected: false,
|
||||
onTap: () => Get.toNamed(Routes.PETUGAS_AKUN_BANK),
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.bar_chart_outlined,
|
||||
activeIcon: Icons.bar_chart,
|
||||
title: 'Laporan Bulanan',
|
||||
subtitle: 'Cetak laporan bulanan',
|
||||
isSelected: false,
|
||||
onTap: () => Get.toNamed(Routes.PETUGAS_LAPORAN),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -41,7 +41,7 @@ class PetugasMitraDashboardController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal keluar dari aplikasi',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ class SplashView extends GetView<SplashController> {
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/pattern.png'),
|
||||
image: AssetImage('assets/images/logo.png'), // Using logo.png which exists
|
||||
repeat: ImageRepeat.repeat,
|
||||
scale: 4.0,
|
||||
),
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/order_sewa_paket_controller.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
import '../../../data/providers/sewa_provider.dart';
|
||||
|
||||
class OrderSewaPaketBinding extends Bindings {
|
||||
@override
|
||||
@ -11,10 +10,6 @@ class OrderSewaPaketBinding extends Bindings {
|
||||
Get.put(AsetProvider());
|
||||
}
|
||||
|
||||
if (!Get.isRegistered<SewaProvider>()) {
|
||||
Get.put(SewaProvider());
|
||||
}
|
||||
|
||||
Get.lazyPut<OrderSewaPaketController>(
|
||||
() => OrderSewaPaketController(),
|
||||
);
|
||||
|
||||
@ -8,17 +8,11 @@ import '../../../data/providers/aset_provider.dart';
|
||||
class WargaSewaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Ensure NavigationService is registered and set to Sewa tab
|
||||
if (Get.isRegistered<NavigationService>()) {
|
||||
final navService = Get.find<NavigationService>();
|
||||
navService.setNavIndex(1); // Set to Sewa tab
|
||||
}
|
||||
|
||||
// Ensure AuthProvider is registered
|
||||
if (!Get.isRegistered<AuthProvider>()) {
|
||||
Get.put(AuthProvider(), permanent: true);
|
||||
}
|
||||
|
||||
|
||||
// Ensure AsetProvider is registered
|
||||
if (!Get.isRegistered<AsetProvider>()) {
|
||||
Get.put(AsetProvider(), permanent: true);
|
||||
|
||||
@ -378,7 +378,7 @@ class PembayaranSewaController extends GetxController
|
||||
Get.snackbar(
|
||||
'Pesanan Dibatalkan',
|
||||
'Batas waktu pembayaran telah berakhir',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
duration: Duration(seconds: 5),
|
||||
@ -417,7 +417,7 @@ class PembayaranSewaController extends GetxController
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal mengambil foto: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -443,7 +443,7 @@ class PembayaranSewaController extends GetxController
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memilih foto dari galeri: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -459,7 +459,7 @@ class PembayaranSewaController extends GetxController
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Mohon unggah bukti pembayaran terlebih dahulu',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -541,7 +541,7 @@ class PembayaranSewaController extends GetxController
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Bukti pembayaran berhasil diunggah',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -550,7 +550,7 @@ class PembayaranSewaController extends GetxController
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal mengunggah bukti pembayaran: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -654,7 +654,7 @@ class PembayaranSewaController extends GetxController
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Pembayaran tunai berhasil disubmit',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -1090,7 +1090,7 @@ class PembayaranSewaController extends GetxController
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Data berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 2),
|
||||
@ -1104,7 +1104,7 @@ class PembayaranSewaController extends GetxController
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memperbarui data',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
@ -88,6 +88,19 @@ class SewaAsetController extends GetxController
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
debugPrint('🚀 SewaAsetController: onReady called');
|
||||
// Set tab index from arguments (if any) after build
|
||||
Future.delayed(Duration.zero, () {
|
||||
final args = Get.arguments;
|
||||
if (args != null && args is Map && args['tab'] != null) {
|
||||
int initialTab =
|
||||
args['tab'] is int
|
||||
? args['tab']
|
||||
: int.tryParse(args['tab'].toString()) ?? 0;
|
||||
if (tabController.length > initialTab) {
|
||||
tabController.index = initialTab;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -155,7 +168,7 @@ class SewaAsetController extends GetxController
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan saat memuat data aset',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -266,7 +279,7 @@ class SewaAsetController extends GetxController
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
message,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -308,7 +321,7 @@ class SewaAsetController extends GetxController
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Pesanan berhasil dibuat',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -370,7 +383,7 @@ class SewaAsetController extends GetxController
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan saat memuat data paket',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -416,7 +429,7 @@ class SewaAsetController extends GetxController
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memuat data paket. Silakan coba lagi nanti.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -457,7 +470,7 @@ class SewaAsetController extends GetxController
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Pesanan paket berhasil dibuat',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import '../../../services/navigation_service.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class WargaDashboardController extends GetxController {
|
||||
// Dependency injection
|
||||
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
||||
final NavigationService navigationService = Get.find<NavigationService>();
|
||||
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
||||
|
||||
// User data
|
||||
final userName = 'Pengguna Warga'.obs;
|
||||
@ -16,6 +24,10 @@ class WargaDashboardController extends GetxController {
|
||||
final userNik = ''.obs;
|
||||
final userPhone = ''.obs;
|
||||
final userAddress = ''.obs;
|
||||
final userTanggalLahir = ''.obs;
|
||||
final userRtRw = ''.obs;
|
||||
final userKelurahanDesa = ''.obs;
|
||||
final userKecamatan = ''.obs;
|
||||
|
||||
// Navigation state is now managed by NavigationService
|
||||
|
||||
@ -28,24 +40,57 @@ class WargaDashboardController extends GetxController {
|
||||
// Active penalties
|
||||
final activePenalties = <Map<String, dynamic>>[].obs;
|
||||
|
||||
// Summary counts
|
||||
final diterimaCount = 0.obs;
|
||||
final tagihanAktifCount = 0.obs;
|
||||
final dendaAktifCount = 0.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
|
||||
// Set navigation index to Home (0)
|
||||
navigationService.setNavIndex(0);
|
||||
|
||||
// Load user data
|
||||
_loadUserData();
|
||||
// Check if navigation is coming from login
|
||||
final args = Get.arguments;
|
||||
final bool isFromLogin = args != null && args['from_login'] == true;
|
||||
|
||||
// Load sample data
|
||||
_loadSampleData();
|
||||
if (isFromLogin) {
|
||||
print('onInit: Navigation from login detected, prioritizing data fetch');
|
||||
}
|
||||
|
||||
// Load dummy data for bills and penalties
|
||||
loadDummyData();
|
||||
// Verifikasi bahwa pengguna sudah login sebelum melakukan fetch data
|
||||
if (_authProvider.currentUser != null) {
|
||||
// Prioritize loading user profile data first
|
||||
await fetchProfileFromWargaDesa();
|
||||
|
||||
// Load unpaid rentals
|
||||
loadUnpaidRentals();
|
||||
// If the profile data was not loaded successfully, try again after a short delay
|
||||
if (userName.value == 'Pengguna Warga' || userNik.value.isEmpty) {
|
||||
print('onInit: Profile data not loaded, retrying after delay');
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
await fetchProfileFromWargaDesa();
|
||||
}
|
||||
|
||||
// Load other user data
|
||||
await _loadUserData();
|
||||
|
||||
// Load other data in parallel to speed up the dashboard initialization
|
||||
Future.wait([
|
||||
_loadSampleData(),
|
||||
loadDummyData(),
|
||||
loadUnpaidRentals(),
|
||||
_debugCountSewaAset(),
|
||||
loadActiveRentals(),
|
||||
]).then((_) => print('onInit: All data loaded successfully'));
|
||||
|
||||
// If coming from login, make sure UI is updated
|
||||
if (isFromLogin) {
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
print('onInit: User not logged in, skipping data fetch');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadUserData() async {
|
||||
@ -75,12 +120,24 @@ class WargaDashboardController extends GetxController {
|
||||
userNik.value = await _authProvider.getUserNIK() ?? '';
|
||||
userPhone.value = await _authProvider.getUserPhone() ?? '';
|
||||
userAddress.value = await _authProvider.getUserAddress() ?? '';
|
||||
|
||||
// Load additional profile data
|
||||
final tanggalLahir = await _authProvider.getUserTanggalLahir();
|
||||
final rtRw = await _authProvider.getUserRtRw();
|
||||
final kelurahanDesa = await _authProvider.getUserKelurahanDesa();
|
||||
final kecamatan = await _authProvider.getUserKecamatan();
|
||||
|
||||
// Set values for additional profile data
|
||||
userTanggalLahir.value = tanggalLahir ?? 'Tidak tersedia';
|
||||
userRtRw.value = rtRw ?? 'Tidak tersedia';
|
||||
userKelurahanDesa.value = kelurahanDesa ?? 'Tidak tersedia';
|
||||
userKecamatan.value = kecamatan ?? 'Tidak tersedia';
|
||||
} catch (e) {
|
||||
print('Error loading user data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _loadSampleData() {
|
||||
Future<void> _loadSampleData() async {
|
||||
// Clear any existing data
|
||||
activeRentals.clear();
|
||||
|
||||
@ -111,10 +168,37 @@ class WargaDashboardController extends GetxController {
|
||||
navigationService.toSewaAset();
|
||||
}
|
||||
|
||||
void refreshData() {
|
||||
// Refresh data from repository
|
||||
_loadSampleData();
|
||||
loadDummyData();
|
||||
Future<void> refreshData() async {
|
||||
print('refreshData: Refreshing dashboard data');
|
||||
try {
|
||||
// First fetch profile data
|
||||
await fetchProfileFromWargaDesa();
|
||||
await _loadUserData();
|
||||
|
||||
// Then load all other data in parallel
|
||||
await Future.wait([
|
||||
_loadSampleData(),
|
||||
loadDummyData(),
|
||||
loadUnpaidRentals(),
|
||||
loadActiveRentals(),
|
||||
_debugCountSewaAset(),
|
||||
]);
|
||||
|
||||
// Update UI
|
||||
update();
|
||||
print('refreshData: Dashboard data refreshed successfully');
|
||||
} catch (e) {
|
||||
print('refreshData: Error refreshing data: $e');
|
||||
// Show error message to user
|
||||
Get.snackbar(
|
||||
'Perhatian',
|
||||
'Terjadi kesalahan saat memuat data',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red.shade100,
|
||||
colorText: Colors.red.shade900,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onNavItemTapped(int index) {
|
||||
@ -129,18 +213,25 @@ class WargaDashboardController extends GetxController {
|
||||
// Already on Home tab
|
||||
break;
|
||||
case 1:
|
||||
// Navigate to Sewa page
|
||||
navigationService.toWargaSewa();
|
||||
// Navigate to Sewa page, tab Aktif
|
||||
toWargaSewaTabAktif();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void logout() async {
|
||||
await _authProvider.signOut();
|
||||
navigationService.toLogin();
|
||||
void toWargaSewaTabAktif() {
|
||||
// Navigasi ke halaman warga sewa dan tab Aktif (index 3)
|
||||
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 3});
|
||||
}
|
||||
|
||||
void loadDummyData() {
|
||||
Future<void> logout() async {
|
||||
print('logout: Logging out user');
|
||||
await _authProvider.signOut();
|
||||
navigationService.toLogin();
|
||||
print('logout: User logged out and redirected to login screen');
|
||||
}
|
||||
|
||||
Future<void> loadDummyData() async {
|
||||
// Dummy active bills
|
||||
activeBills.clear();
|
||||
activeBills.add({
|
||||
@ -177,4 +268,555 @@ class WargaDashboardController extends GetxController {
|
||||
print('Error loading unpaid rentals: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _debugCountSewaAset() async {
|
||||
diterimaCount.value = await _asetProvider.countSewaAsetByStatus([
|
||||
'DITERIMA',
|
||||
]);
|
||||
tagihanAktifCount.value = await _asetProvider.countSewaAsetByStatus([
|
||||
'MENUNGGU PEMBAYARAN',
|
||||
'PERIKSA PEMBAYARAN',
|
||||
]);
|
||||
dendaAktifCount.value = await _asetProvider.countSewaAsetByStatus([
|
||||
'PEMBAYARAN DENDA',
|
||||
'PERIKSA PEMBAYARAN DENDA',
|
||||
]);
|
||||
print('[DEBUG] Jumlah sewa diterima: ${diterimaCount.value}');
|
||||
print('[DEBUG] Jumlah tagihan aktif: ${tagihanAktifCount.value}');
|
||||
print('[DEBUG] Jumlah denda aktif: ${dendaAktifCount.value}');
|
||||
}
|
||||
|
||||
Future<void> loadActiveRentals() async {
|
||||
try {
|
||||
activeRentals.clear();
|
||||
final sewaAsetList = await _authProvider.getSewaAsetByStatus(['AKTIF']);
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await _asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
if (sewaAset['waktu_mulai'] != null &&
|
||||
sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
String duration = '-';
|
||||
final tagihan = await _asetProvider.getTagihanSewa(sewaAset['id']);
|
||||
if (tagihan != null) {
|
||||
final durasiTagihan = tagihan['durasi'] ?? sewaAset['durasi'];
|
||||
final satuanTagihan = tagihan['nama_satuan_waktu'] ?? namaSatuanWaktu;
|
||||
duration = '${durasiTagihan ?? '-'} ${satuanTagihan ?? ''}';
|
||||
} else {
|
||||
duration = '${sewaAset['durasi'] ?? '-'} ${namaSatuanWaktu ?? ''}';
|
||||
}
|
||||
activeRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': duration,
|
||||
'status': sewaAset['status'] ?? 'AKTIF',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
'can_extend': sewaAset['can_extend'] == true,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading active rentals: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void toSewaAsetTabPaket() {
|
||||
// Navigasi ke halaman sewa_aset tab Paket (index 1)
|
||||
Get.toNamed(Routes.SEWA_ASET, arguments: {'tab': 1});
|
||||
}
|
||||
|
||||
Future<void> fetchProfileFromWargaDesa() async {
|
||||
try {
|
||||
final user = _authProvider.currentUser;
|
||||
if (user == null) {
|
||||
print(
|
||||
'fetchProfileFromWargaDesa: No current user found, skipping fetch',
|
||||
);
|
||||
return; // Exit early if no user is logged in
|
||||
}
|
||||
|
||||
final userId = user.id;
|
||||
print('fetchProfileFromWargaDesa: Fetching data for user: $userId');
|
||||
|
||||
final data =
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.select('nik, alamat, email, nama_lengkap, no_hp, avatar')
|
||||
.eq('user_id', userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (data != null) {
|
||||
print('fetchProfileFromWargaDesa: Data retrieved successfully');
|
||||
userNik.value = data['nik']?.toString() ?? '';
|
||||
userAddress.value = data['alamat']?.toString() ?? '';
|
||||
userEmail.value = data['email']?.toString() ?? '';
|
||||
userName.value = data['nama_lengkap']?.toString() ?? '';
|
||||
userPhone.value = data['no_hp']?.toString() ?? '';
|
||||
userAvatar.value = data['avatar']?.toString() ?? '';
|
||||
|
||||
// Trigger UI refresh
|
||||
update();
|
||||
print('fetchProfileFromWargaDesa: Profile data updated');
|
||||
} else {
|
||||
print('fetchProfileFromWargaDesa: No data found for user: $userId');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching profile from warga_desa: $e');
|
||||
// If it fails, try again after a delay
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
try {
|
||||
await _retryFetchProfile();
|
||||
} catch (retryError) {
|
||||
print('Retry error fetching profile: $retryError');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to retry fetching profile
|
||||
Future<void> _retryFetchProfile() async {
|
||||
final user = _authProvider.currentUser;
|
||||
if (user == null) {
|
||||
print('_retryFetchProfile: No current user found, skipping retry');
|
||||
return; // Exit early if no user is logged in
|
||||
}
|
||||
|
||||
print('_retryFetchProfile: Retrying fetch for user: ${user.id}');
|
||||
|
||||
final data =
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.select('nik, alamat, email, nama_lengkap, no_hp, avatar')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (data != null) {
|
||||
print('_retryFetchProfile: Data retrieved successfully on retry');
|
||||
userNik.value = data['nik']?.toString() ?? '';
|
||||
userAddress.value = data['alamat']?.toString() ?? '';
|
||||
userEmail.value = data['email']?.toString() ?? '';
|
||||
userName.value = data['nama_lengkap']?.toString() ?? '';
|
||||
userPhone.value = data['no_hp']?.toString() ?? '';
|
||||
userAvatar.value = data['avatar']?.toString() ?? '';
|
||||
update();
|
||||
print('_retryFetchProfile: Profile data updated');
|
||||
}
|
||||
}
|
||||
|
||||
// Method to update user profile data in warga_desa table
|
||||
Future<bool> updateUserProfile({
|
||||
required String namaLengkap,
|
||||
required String noHp,
|
||||
}) async {
|
||||
try {
|
||||
final user = _authProvider.currentUser;
|
||||
if (user == null) {
|
||||
print('Cannot update profile: No current user');
|
||||
return false;
|
||||
}
|
||||
|
||||
final userId = user.id;
|
||||
|
||||
// Update data in warga_desa table
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.update({'nama_lengkap': namaLengkap, 'no_hp': noHp})
|
||||
.eq('user_id', userId);
|
||||
|
||||
// Update local values
|
||||
userName.value = namaLengkap;
|
||||
userPhone.value = noHp;
|
||||
|
||||
print('Profile updated successfully for user: $userId');
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('Error updating user profile: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to delete user avatar
|
||||
Future<bool> deleteUserAvatar() async {
|
||||
try {
|
||||
final user = _authProvider.currentUser;
|
||||
if (user == null) {
|
||||
print('Cannot delete avatar: No current user');
|
||||
return false;
|
||||
}
|
||||
|
||||
final userId = user.id;
|
||||
final currentAvatarUrl = userAvatar.value;
|
||||
|
||||
// If there's an avatar URL, delete it from storage
|
||||
if (currentAvatarUrl != null && currentAvatarUrl.isNotEmpty) {
|
||||
try {
|
||||
print('Attempting to delete avatar from URL: $currentAvatarUrl');
|
||||
|
||||
// Extract filename from URL
|
||||
// The URL format is typically:
|
||||
// https://[project-ref].supabase.co/storage/v1/object/public/warga/[filename]
|
||||
|
||||
final uri = Uri.parse(currentAvatarUrl);
|
||||
final path = uri.path;
|
||||
|
||||
// Find the filename after the last slash
|
||||
final filename = path.substring(path.lastIndexOf('/') + 1);
|
||||
|
||||
if (filename.isNotEmpty) {
|
||||
print('Extracted filename: $filename');
|
||||
|
||||
// Delete from storage bucket 'warga'
|
||||
final response = await _authProvider.client.storage
|
||||
.from('warga')
|
||||
.remove([filename]);
|
||||
|
||||
print('Storage deletion response: $response');
|
||||
} else {
|
||||
print('Failed to extract filename from avatar URL');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error deleting avatar from storage: $e');
|
||||
// Continue with database update even if storage delete fails
|
||||
}
|
||||
}
|
||||
|
||||
// Update warga_desa table to set avatar to null
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.update({'avatar': null})
|
||||
.eq('user_id', userId);
|
||||
|
||||
// Update local value
|
||||
userAvatar.value = '';
|
||||
|
||||
print('Avatar deleted successfully for user: $userId');
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('Error deleting user avatar: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to update user avatar URL
|
||||
Future<bool> updateUserAvatar(String avatarUrl) async {
|
||||
try {
|
||||
final user = _authProvider.currentUser;
|
||||
if (user == null) {
|
||||
print('Cannot update avatar: No current user');
|
||||
return false;
|
||||
}
|
||||
|
||||
final userId = user.id;
|
||||
|
||||
// Update data in warga_desa table
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.update({'avatar': avatarUrl})
|
||||
.eq('user_id', userId);
|
||||
|
||||
// Update local value
|
||||
userAvatar.value = avatarUrl;
|
||||
|
||||
print('Avatar updated successfully for user: $userId');
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('Error updating user avatar: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to upload avatar image to Supabase storage
|
||||
Future<String?> uploadAvatar(Uint8List fileBytes, String fileName) async {
|
||||
try {
|
||||
final user = _authProvider.currentUser;
|
||||
if (user == null) {
|
||||
print('Cannot upload avatar: No current user');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate a unique filename using timestamp and user ID
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final extension = fileName.split('.').last;
|
||||
final uniqueFileName = 'avatar_${user.id}_$timestamp.$extension';
|
||||
|
||||
// Upload to 'warga' bucket
|
||||
final response = await _authProvider.client.storage
|
||||
.from('warga')
|
||||
.uploadBinary(
|
||||
uniqueFileName,
|
||||
fileBytes,
|
||||
fileOptions: const FileOptions(cacheControl: '3600', upsert: true),
|
||||
);
|
||||
|
||||
// Get the public URL
|
||||
final publicUrl = _authProvider.client.storage
|
||||
.from('warga')
|
||||
.getPublicUrl(uniqueFileName);
|
||||
|
||||
print('Avatar uploaded successfully: $publicUrl');
|
||||
return publicUrl;
|
||||
} catch (e) {
|
||||
print('Error uploading avatar: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to handle image picking from camera or gallery
|
||||
Future<XFile?> pickImage(ImageSource source) async {
|
||||
try {
|
||||
// Pick image directly without permission checks
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? pickedFile = await picker.pickImage(
|
||||
source: source,
|
||||
maxWidth: 800,
|
||||
maxHeight: 800,
|
||||
imageQuality: 85,
|
||||
);
|
||||
|
||||
if (pickedFile != null) {
|
||||
print('Image picked: ${pickedFile.path}');
|
||||
}
|
||||
return pickedFile;
|
||||
} catch (e) {
|
||||
print('Error picking image: $e');
|
||||
|
||||
// Show error message if there's an issue
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Tidak dapat mengakses ${source == ImageSource.camera ? 'kamera' : 'galeri'}',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red.shade700,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to show image source selection dialog
|
||||
Future<void> showImageSourceDialog() async {
|
||||
await Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'Pilih Sumber Gambar',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildImageSourceOption(
|
||||
icon: Icons.camera_alt_rounded,
|
||||
label: 'Kamera',
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
final pickedFile = await pickImage(ImageSource.camera);
|
||||
if (pickedFile != null) {
|
||||
await processPickedImage(pickedFile);
|
||||
}
|
||||
},
|
||||
),
|
||||
_buildImageSourceOption(
|
||||
icon: Icons.photo_library_rounded,
|
||||
label: 'Galeri',
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
final pickedFile = await pickImage(ImageSource.gallery);
|
||||
if (pickedFile != null) {
|
||||
await processPickedImage(pickedFile);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
isDismissible: true,
|
||||
enableDrag: true,
|
||||
);
|
||||
}
|
||||
|
||||
// Helper method to build image source option
|
||||
Widget _buildImageSourceOption({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: AppColors.primary, size: 32),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Method to process picked image (temporary preview before saving)
|
||||
Future<void> processPickedImage(XFile pickedFile) async {
|
||||
try {
|
||||
// Read file as bytes
|
||||
final bytes = await pickedFile.readAsBytes();
|
||||
|
||||
// Store the picked file temporarily for later use when saving
|
||||
tempPickedFile.value = pickedFile;
|
||||
|
||||
// Update UI with temporary avatar preview
|
||||
tempAvatarBytes.value = bytes;
|
||||
|
||||
print('Image processed for preview');
|
||||
} catch (e) {
|
||||
print('Error processing picked image: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Method to save the picked image to Supabase and update profile
|
||||
Future<bool> saveNewAvatar() async {
|
||||
try {
|
||||
if (tempPickedFile.value == null || tempAvatarBytes.value == null) {
|
||||
print('No temporary image to save');
|
||||
return false;
|
||||
}
|
||||
|
||||
final pickedFile = tempPickedFile.value!;
|
||||
final bytes = tempAvatarBytes.value!;
|
||||
|
||||
// First delete the old avatar if exists
|
||||
final currentAvatarUrl = userAvatar.value;
|
||||
if (currentAvatarUrl != null && currentAvatarUrl.isNotEmpty) {
|
||||
try {
|
||||
await deleteUserAvatar();
|
||||
} catch (e) {
|
||||
print('Error deleting old avatar: $e');
|
||||
// Continue with upload even if delete fails
|
||||
}
|
||||
}
|
||||
|
||||
// Upload new avatar
|
||||
final newAvatarUrl = await uploadAvatar(bytes, pickedFile.name);
|
||||
if (newAvatarUrl == null) {
|
||||
print('Failed to upload new avatar');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update avatar URL in database
|
||||
final success = await updateUserAvatar(newAvatarUrl);
|
||||
|
||||
if (success) {
|
||||
// Clear temporary data
|
||||
tempPickedFile.value = null;
|
||||
tempAvatarBytes.value = null;
|
||||
|
||||
print('Avatar updated successfully');
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (e) {
|
||||
print('Error saving new avatar: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to cancel avatar change
|
||||
void cancelAvatarChange() {
|
||||
tempPickedFile.value = null;
|
||||
tempAvatarBytes.value = null;
|
||||
print('Avatar change canceled');
|
||||
}
|
||||
|
||||
// Temporary storage for picked image
|
||||
final Rx<XFile?> tempPickedFile = Rx<XFile?>(null);
|
||||
final Rx<Uint8List?> tempAvatarBytes = Rx<Uint8List?>(null);
|
||||
}
|
||||
|
||||
@ -12,10 +12,10 @@ class WargaSewaController extends GetxController
|
||||
|
||||
// Get navigation service
|
||||
final NavigationService navigationService = Get.find<NavigationService>();
|
||||
|
||||
|
||||
// Get auth provider for user data and sewa_aset queries
|
||||
final AuthProvider authProvider = Get.find<AuthProvider>();
|
||||
|
||||
|
||||
// Get aset provider for asset data
|
||||
final AsetProvider asetProvider = Get.find<AsetProvider>();
|
||||
|
||||
@ -25,33 +25,35 @@ class WargaSewaController extends GetxController
|
||||
final acceptedRentals = <Map<String, dynamic>>[].obs;
|
||||
final completedRentals = <Map<String, dynamic>>[].obs;
|
||||
final cancelledRentals = <Map<String, dynamic>>[].obs;
|
||||
|
||||
final returnedRentals = <Map<String, dynamic>>[].obs;
|
||||
final activeRentals = <Map<String, dynamic>>[].obs;
|
||||
|
||||
// Loading states
|
||||
final isLoading = false.obs;
|
||||
final isLoadingPending = false.obs;
|
||||
final isLoadingAccepted = false.obs;
|
||||
final isLoadingCompleted = false.obs;
|
||||
final isLoadingCancelled = false.obs;
|
||||
final isLoadingReturned = false.obs;
|
||||
final isLoadingActive = false.obs;
|
||||
|
||||
bool _tabSetFromArgument = false;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// Ensure tab index is set to Sewa (1)
|
||||
navigationService.setNavIndex(1);
|
||||
|
||||
// Initialize tab controller with 6 tabs
|
||||
tabController = TabController(length: 6, vsync: this);
|
||||
|
||||
// Set initial tab and ensure tab view is updated
|
||||
tabController.index = 0;
|
||||
// Initialize tab controller with 7 tabs
|
||||
tabController = TabController(length: 7, vsync: this);
|
||||
|
||||
// Load real rental data for all tabs
|
||||
loadRentalsData();
|
||||
loadPendingRentals();
|
||||
loadAcceptedRentals();
|
||||
loadActiveRentals();
|
||||
loadCompletedRentals();
|
||||
loadCancelledRentals();
|
||||
loadReturnedRentals();
|
||||
|
||||
// Listen to tab changes to update state if needed
|
||||
tabController.addListener(() {
|
||||
@ -77,7 +79,9 @@ class WargaSewaController extends GetxController
|
||||
}
|
||||
break;
|
||||
case 3: // Aktif
|
||||
// Add Aktif tab logic when needed
|
||||
if (activeRentals.isEmpty && !isLoadingActive.value) {
|
||||
loadActiveRentals();
|
||||
}
|
||||
break;
|
||||
case 4: // Selesai
|
||||
if (completedRentals.isEmpty && !isLoadingCompleted.value) {
|
||||
@ -89,6 +93,11 @@ class WargaSewaController extends GetxController
|
||||
loadCancelledRentals();
|
||||
}
|
||||
break;
|
||||
case 6: // Dikembalikan
|
||||
if (returnedRentals.isEmpty && !isLoadingReturned.value) {
|
||||
loadReturnedRentals();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -96,9 +105,26 @@ class WargaSewaController extends GetxController
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
// Ensure nav index is set to Sewa (1) when the controller is ready
|
||||
// This helps maintain correct state during hot reload
|
||||
navigationService.setNavIndex(1);
|
||||
// Jalankan update nav index dan tab index setelah build selesai
|
||||
Future.delayed(Duration.zero, () {
|
||||
navigationService.setNavIndex(1);
|
||||
|
||||
final args = Get.arguments;
|
||||
int initialTab = 0;
|
||||
if (!_tabSetFromArgument &&
|
||||
args != null &&
|
||||
args is Map &&
|
||||
args['tab'] != null) {
|
||||
initialTab =
|
||||
args['tab'] is int
|
||||
? args['tab']
|
||||
: int.tryParse(args['tab'].toString()) ?? 0;
|
||||
if (tabController.length > initialTab) {
|
||||
tabController.index = initialTab;
|
||||
_tabSetFromArgument = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -107,110 +133,152 @@ class WargaSewaController extends GetxController
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Helper method to process rental data
|
||||
Future<Map<String, dynamic>> _processRentalData(
|
||||
Map<String, dynamic> sewaAset,
|
||||
) async {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
// Check if this is a package or single asset rental
|
||||
bool isPaket = sewaAset['aset_id'] == null && sewaAset['paket_id'] != null;
|
||||
|
||||
if (isPaket) {
|
||||
// Use package data that was fetched in getSewaAsetByStatus
|
||||
assetName = sewaAset['nama_paket'] ?? 'Paket';
|
||||
imageUrl = sewaAset['foto_paket'];
|
||||
debugPrint(
|
||||
'Using package data: name=${assetName}, imageUrl=${imageUrl ?? "none"}',
|
||||
);
|
||||
} else if (sewaAset['aset_id'] != null) {
|
||||
// Regular asset rental
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Return processed rental data
|
||||
return {
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? '',
|
||||
'totalPrice': totalPrice,
|
||||
'countdown': '00:59:59', // Default countdown
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
'updated_at': sewaAset['updated_at'],
|
||||
'isPaket': isPaket,
|
||||
'paketId': isPaket ? sewaAset['paket_id'] : null,
|
||||
};
|
||||
}
|
||||
|
||||
// Load real data from sewa_aset table
|
||||
Future<void> loadRentalsData() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
|
||||
// Clear existing data
|
||||
rentals.clear();
|
||||
|
||||
|
||||
// Get sewa_aset data with status "MENUNGGU PEMBAYARAN" or "PEMBAYARAN DENDA"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||
'MENUNGGU PEMBAYARAN',
|
||||
'PEMBAYARAN DENDA'
|
||||
'PEMBAYARAN DENDA',
|
||||
]);
|
||||
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} sewa_aset records');
|
||||
|
||||
|
||||
// Debug the structure of the first record if available
|
||||
if (sewaAsetList.isNotEmpty) {
|
||||
debugPrint('Sample sewa_aset record: ${sewaAsetList.first}');
|
||||
debugPrint('updated_at field: ${sewaAsetList.first['updated_at']}');
|
||||
}
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'MENUNGGU PEMBAYARAN';
|
||||
|
||||
// Ensure updated_at is set correctly
|
||||
if (sewaAset['updated_at'] == null &&
|
||||
processedData['status'] == 'MENUNGGU PEMBAYARAN') {
|
||||
// If updated_at is null but status is MENUNGGU PEMBAYARAN, use created_at as fallback
|
||||
processedData['updated_at'] =
|
||||
sewaAset['created_at'] ?? DateTime.now().toIso8601String();
|
||||
debugPrint(
|
||||
'Using created_at as fallback for updated_at: ${processedData['updated_at']}',
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to rentals list
|
||||
rentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'MENUNGGU PEMBAYARAN',
|
||||
'totalPrice': totalPrice,
|
||||
'countdown': '00:59:59', // Default countdown
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
});
|
||||
|
||||
rentals.add(processedData);
|
||||
}
|
||||
|
||||
|
||||
debugPrint('Processed ${rentals.length} rental records');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading rentals data: $e');
|
||||
@ -245,353 +313,237 @@ class WargaSewaController extends GetxController
|
||||
}
|
||||
|
||||
// Actions
|
||||
void cancelRental(String id) {
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Pembatalan berhasil',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
void cancelRental(String id) async {
|
||||
final confirmed = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: const Text('Konfirmasi Pembatalan'),
|
||||
content: const Text('Apakah Anda yakin ingin membatalkan pesanan ini?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Tidak'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('Ya, Batalkan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
await asetProvider.client
|
||||
.from('sewa_aset')
|
||||
.update({'status': 'DIBATALKAN'})
|
||||
.eq('id', id);
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Pesanan berhasil dibatalkan',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
// Refresh data
|
||||
loadRentalsData();
|
||||
loadPendingRentals();
|
||||
loadAcceptedRentals();
|
||||
loadActiveRentals();
|
||||
loadCompletedRentals();
|
||||
loadCancelledRentals();
|
||||
loadReturnedRentals();
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Gagal membatalkan pesanan: $e',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to payment page with the selected rental data
|
||||
void viewRentalDetail(Map<String, dynamic> rental) {
|
||||
debugPrint('Navigating to payment page with rental ID: ${rental['id']}');
|
||||
|
||||
|
||||
// Navigate to payment page with rental data
|
||||
Get.toNamed(
|
||||
Routes.PEMBAYARAN_SEWA,
|
||||
arguments: {'orderId': rental['id'], 'rentalData': rental},
|
||||
);
|
||||
}
|
||||
|
||||
// Navigate directly to payment tab of payment page with the selected rental data
|
||||
void viewPaymentTab(Map<String, dynamic> rental) {
|
||||
debugPrint('Navigating to payment tab with rental ID: ${rental['id']}');
|
||||
|
||||
// Navigate to payment page with rental data and initialTab set to 2 (payment tab)
|
||||
Get.toNamed(
|
||||
Routes.PEMBAYARAN_SEWA,
|
||||
arguments: {
|
||||
'orderId': rental['id'],
|
||||
'rentalData': rental,
|
||||
'initialTab': 2, // Index 2 corresponds to the payment tab
|
||||
'isPaket': rental['isPaket'] ?? false,
|
||||
'paketId': rental['paketId'],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void payRental(String id) {
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Navigasi ke halaman pembayaran',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Load data for the Selesai tab (status: SELESAI)
|
||||
Future<void> loadCompletedRentals() async {
|
||||
try {
|
||||
isLoadingCompleted.value = true;
|
||||
|
||||
|
||||
// Clear existing data
|
||||
completedRentals.clear();
|
||||
|
||||
|
||||
// Get sewa_aset data with status "SELESAI"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['SELESAI']);
|
||||
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} completed sewa_aset records');
|
||||
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to completed rentals list
|
||||
completedRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'SELESAI',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
});
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'SELESAI';
|
||||
completedRentals.add(processedData);
|
||||
}
|
||||
|
||||
debugPrint('Processed ${completedRentals.length} completed rental records');
|
||||
|
||||
debugPrint(
|
||||
'Processed ${completedRentals.length} completed rental records',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error loading completed rentals data: $e');
|
||||
} finally {
|
||||
isLoadingCompleted.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load data for the Dibatalkan tab (status: DIBATALKAN)
|
||||
Future<void> loadCancelledRentals() async {
|
||||
try {
|
||||
isLoadingCancelled.value = true;
|
||||
|
||||
|
||||
// Clear existing data
|
||||
cancelledRentals.clear();
|
||||
|
||||
|
||||
// Get sewa_aset data with status "DIBATALKAN"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['DIBATALKAN']);
|
||||
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||
'DIBATALKAN',
|
||||
]);
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} cancelled sewa_aset records');
|
||||
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to cancelled rentals list
|
||||
cancelledRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'DIBATALKAN',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
'alasanPembatalan': sewaAset['alasan_pembatalan'] ?? '-',
|
||||
});
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'DIBATALKAN';
|
||||
processedData['alasanPembatalan'] =
|
||||
sewaAset['alasan_pembatalan'] ?? '-';
|
||||
cancelledRentals.add(processedData);
|
||||
}
|
||||
|
||||
debugPrint('Processed ${cancelledRentals.length} cancelled rental records');
|
||||
|
||||
debugPrint(
|
||||
'Processed ${cancelledRentals.length} cancelled rental records',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error loading cancelled rentals data: $e');
|
||||
} finally {
|
||||
isLoadingCancelled.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load data for the Dikembalikan tab (status: DIKEMBALIKAN)
|
||||
Future<void> loadReturnedRentals() async {
|
||||
try {
|
||||
isLoadingReturned.value = true;
|
||||
|
||||
// Clear existing data
|
||||
returnedRentals.clear();
|
||||
|
||||
// Get sewa_aset data with status "DIKEMBALIKAN"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||
'DIKEMBALIKAN',
|
||||
]);
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} returned sewa_aset records');
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'DIKEMBALIKAN';
|
||||
returnedRentals.add(processedData);
|
||||
}
|
||||
|
||||
debugPrint('Processed ${returnedRentals.length} returned rental records');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading returned rentals data: $e');
|
||||
} finally {
|
||||
isLoadingReturned.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load data for the Aktif tab (status: AKTIF)
|
||||
Future<void> loadActiveRentals() async {
|
||||
try {
|
||||
isLoadingActive.value = true;
|
||||
|
||||
// Clear existing data
|
||||
activeRentals.clear();
|
||||
|
||||
// Get sewa_aset data with status "AKTIF"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['AKTIF']);
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} active sewa_aset records');
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'AKTIF';
|
||||
activeRentals.add(processedData);
|
||||
}
|
||||
|
||||
debugPrint('Processed ${activeRentals.length} active rental records');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading active rentals data: $e');
|
||||
} finally {
|
||||
isLoadingActive.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load data for the Pending tab (status: PERIKSA PEMBAYARAN)
|
||||
Future<void> loadPendingRentals() async {
|
||||
try {
|
||||
isLoadingPending.value = true;
|
||||
|
||||
|
||||
// Clear existing data
|
||||
pendingRentals.clear();
|
||||
|
||||
// Get sewa_aset data with status "PERIKSA PEMBAYARAN"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['PERIKSA PEMBAYARAN']);
|
||||
|
||||
|
||||
// Get sewa_aset data with status 'PERIKSA PEMBAYARAN' dan 'PERIKSA PEMBAYARAN DENDA'
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||
'PERIKSA PEMBAYARAN',
|
||||
'PERIKSA PEMBAYARAN DENDA',
|
||||
]);
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} pending sewa_aset records');
|
||||
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to pending rentals list
|
||||
pendingRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'PERIKSA PEMBAYARAN',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
});
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'PERIKSA PEMBAYARAN';
|
||||
pendingRentals.add(processedData);
|
||||
}
|
||||
|
||||
|
||||
debugPrint('Processed ${pendingRentals.length} pending rental records');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading pending rentals data: $e');
|
||||
@ -599,107 +551,27 @@ class WargaSewaController extends GetxController
|
||||
isLoadingPending.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load data for the Diterima tab (status: DITERIMA)
|
||||
Future<void> loadAcceptedRentals() async {
|
||||
try {
|
||||
isLoadingAccepted.value = true;
|
||||
|
||||
|
||||
// Clear existing data
|
||||
acceptedRentals.clear();
|
||||
|
||||
|
||||
// Get sewa_aset data with status "DITERIMA"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['DITERIMA']);
|
||||
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} accepted sewa_aset records');
|
||||
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to accepted rentals list
|
||||
acceptedRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'DITERIMA',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
});
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'DITERIMA';
|
||||
acceptedRentals.add(processedData);
|
||||
}
|
||||
|
||||
|
||||
debugPrint('Processed ${acceptedRentals.length} accepted rental records');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading accepted rentals data: $e');
|
||||
|
||||
@ -318,7 +318,7 @@
|
||||
Get.snackbar(
|
||||
'Perhatian',
|
||||
'Pilih jam mulai terlebih dahulu',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: AppColors.warning,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
@ -68,7 +68,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
child: TextField(
|
||||
controller: controller.searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari aset...',
|
||||
hintText: 'Cari aset atau paket...',
|
||||
hintStyle: TextStyle(color: Colors.grey[400]),
|
||||
prefixIcon: Icon(Icons.search, color: Colors.grey[600]),
|
||||
border: InputBorder.none,
|
||||
@ -117,6 +117,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
),
|
||||
],
|
||||
),
|
||||
dividerColor: Colors.transparent,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: const Color(
|
||||
0xFF718093,
|
||||
@ -363,246 +364,271 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.50, // Make cards taller to avoid overflow
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: controller.filteredPakets.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
final paket = controller.filteredPakets[index];
|
||||
final List<dynamic> satuanWaktuSewa =
|
||||
paket['satuanWaktuSewa'] ?? [];
|
||||
return RefreshIndicator(
|
||||
onRefresh: controller.loadPakets,
|
||||
color: const Color(0xFF3A6EA5), // Primary blue
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.only(top: 16.0, bottom: 16.0),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.50, // Make cards taller to avoid overflow
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: controller.filteredPakets.length,
|
||||
itemBuilder: (context, index) {
|
||||
final paket = controller.filteredPakets[index];
|
||||
final List<dynamic> satuanWaktuSewa =
|
||||
paket['satuanWaktuSewa'] ?? [];
|
||||
|
||||
// Find the lowest price
|
||||
int lowestPrice =
|
||||
satuanWaktuSewa.isEmpty
|
||||
? 0
|
||||
: satuanWaktuSewa
|
||||
.map<int>((sws) => sws['harga'] ?? 0)
|
||||
.reduce((a, b) => a < b ? a : b);
|
||||
// Find the lowest price
|
||||
int lowestPrice =
|
||||
satuanWaktuSewa.isEmpty
|
||||
? 0
|
||||
: satuanWaktuSewa
|
||||
.map<int>((sws) => sws['harga'] ?? 0)
|
||||
.reduce((a, b) => a < b ? a : b);
|
||||
|
||||
// Get image URL or default
|
||||
String imageUrl = paket['gambar_url'] ?? '';
|
||||
// Get image URL or default
|
||||
String imageUrl = paket['gambar_url'] ?? '';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _showPaketDetailModal(paket),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Image section
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(12),
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// No action when tapping on the card
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder:
|
||||
(context, url) => const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.purple,
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Image section
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(12),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder:
|
||||
(context, url) => const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.purple,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
errorWidget:
|
||||
(context, url, error) => Container(
|
||||
color: Colors.grey[200],
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 32,
|
||||
color: Colors.grey[400],
|
||||
errorWidget:
|
||||
(context, url, error) => Container(
|
||||
color: Colors.grey[200],
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 32,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Content section
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Package name
|
||||
Text(
|
||||
paket['nama'] ?? 'Paket',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
// Content section
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Package name
|
||||
Text(
|
||||
paket['nama'] ?? 'Paket',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Status availability
|
||||
Row(
|
||||
children: [
|
||||
// Status availability
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.success,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Tersedia',
|
||||
style: TextStyle(
|
||||
color: AppColors.success,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// Package pricing - show all pricing options with scrolling
|
||||
if (satuanWaktuSewa.isNotEmpty)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
...satuanWaktuSewa.map((sws) {
|
||||
// Pastikan data yang ditampilkan valid
|
||||
final harga = sws['harga'] ?? 0;
|
||||
final namaSatuan =
|
||||
sws['nama_satuan_waktu'] ??
|
||||
'Satuan';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: 4,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(
|
||||
4,
|
||||
),
|
||||
border: Border.all(
|
||||
color: Colors.grey[300]!,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Rp ${_formatNumber(harga)}",
|
||||
style: const TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"/$namaSatuan",
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.success,
|
||||
shape: BoxShape.circle,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: Colors.grey[300]!,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Mulai dari Rp ${NumberFormat('#,###').format(lowestPrice)}',
|
||||
style: const TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Tersedia',
|
||||
style: TextStyle(
|
||||
color: AppColors.success,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// Package pricing - show all pricing options with scrolling
|
||||
if (satuanWaktuSewa.isNotEmpty)
|
||||
const Spacer(),
|
||||
|
||||
// Remove the items count badge and replace with direct Order button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
...satuanWaktuSewa.map((sws) {
|
||||
// Pastikan data yang ditampilkan valid
|
||||
final harga = sws['harga'] ?? 0;
|
||||
final namaSatuan =
|
||||
sws['nama_satuan_waktu'] ?? 'Satuan';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: 4,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(
|
||||
4,
|
||||
),
|
||||
border: Border.all(
|
||||
color: Colors.grey[300]!,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Rp ${_formatNumber(harga)}",
|
||||
style: const TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"/$namaSatuan",
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Mulai dari Rp ${NumberFormat('#,###').format(lowestPrice)}',
|
||||
style: const TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// Navigate to order sewa aset page with package data and isPaket flag
|
||||
Get.toNamed(
|
||||
Routes.ORDER_SEWA_ASET,
|
||||
arguments: {
|
||||
'asetId': paket['id'],
|
||||
'paketId': paket['id'],
|
||||
'paketData': paket,
|
||||
'satuanWaktuSewa': satuanWaktuSewa,
|
||||
'isPaket':
|
||||
true, // Add flag to indicate this is a package
|
||||
},
|
||||
preventDuplicates: false,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Remove the items count badge and replace with direct Order button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _showPaketDetailModal(paket),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 6,
|
||||
),
|
||||
minimumSize: const Size(
|
||||
double.infinity,
|
||||
30,
|
||||
),
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 6,
|
||||
),
|
||||
minimumSize: const Size(double.infinity, 30),
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: const Text(
|
||||
'Pesan',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
child: const Text(
|
||||
'Pesan',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
@ -893,35 +919,37 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
|
||||
// Order button
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
padding: const EdgeInsets.only(top: 16.0, bottom: 24.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (satuanWaktuSewa.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Tidak Dapat Memesan',
|
||||
'Pilihan harga belum tersedia untuk paket ini',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red[100],
|
||||
colorText: Colors.red[800],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_showOrderPaketForm(paket, satuanWaktuSewa);
|
||||
// Close the modal
|
||||
Get.back();
|
||||
// Navigate to order_sewa_paket page with package data
|
||||
Get.toNamed(
|
||||
Routes.ORDER_SEWA_PAKET,
|
||||
arguments: {
|
||||
'paket': paket,
|
||||
'satuanWaktuSewa': satuanWaktuSewa,
|
||||
},
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: const Text(
|
||||
'Pesan Paket Ini',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
'Pesan Sekarang',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -929,6 +957,9 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
],
|
||||
),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
barrierColor: Colors.black54,
|
||||
);
|
||||
}
|
||||
|
||||
@ -945,10 +976,11 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
final RxInt duration = RxInt(selectedSWS.value?['durasi_min'] ?? 1);
|
||||
|
||||
// Calculate total price
|
||||
final calculateTotal = () {
|
||||
calculateTotal() {
|
||||
if (selectedSWS.value == null) return 0;
|
||||
return (selectedSWS.value!['harga'] ?? 0) * duration.value;
|
||||
};
|
||||
}
|
||||
|
||||
final RxInt totalPrice = RxInt(calculateTotal());
|
||||
|
||||
// Update total when duration or pricing option changes
|
||||
@ -1231,7 +1263,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'Minimum ${minDuration} ${namaSatuanWaktu.toLowerCase()}',
|
||||
'Minimum $minDuration ${namaSatuanWaktu.toLowerCase()}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
@ -1285,20 +1317,12 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
onPressed: () {
|
||||
Get.back(); // Close the form
|
||||
|
||||
// Navigate to order_sewa_paket page
|
||||
// Get the navigation service from the controller
|
||||
final navigationService = controller.navigationService;
|
||||
|
||||
// Store the selected parameters in a controller or pass as arguments
|
||||
Get.toNamed(
|
||||
Routes.ORDER_SEWA_PAKET,
|
||||
arguments: {
|
||||
'paketId': paket['id'],
|
||||
'satuanWaktuSewaId': selectedSWS.value?['id'] ?? '',
|
||||
'durasi': duration.value,
|
||||
'totalHarga': totalPrice.value,
|
||||
'paketData': paket,
|
||||
},
|
||||
// Order the package
|
||||
controller.placeOrderPaket(
|
||||
paketId: paket['id'],
|
||||
satuanWaktuSewaId: selectedSWS.value?['id'] ?? '',
|
||||
durasi: duration.value,
|
||||
totalHarga: totalPrice.value,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
@ -1500,7 +1524,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'ID aset tidak valid',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -1730,7 +1754,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'ID aset tidak valid',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -1777,15 +1801,22 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'ID aset tidak valid',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the static navigation method to ensure consistent behavior
|
||||
OrderSewaAsetController.navigateToOrderPage(aset.id);
|
||||
// Navigate to order page with asset ID and isAset flag
|
||||
Get.toNamed(
|
||||
Routes.ORDER_SEWA_ASET,
|
||||
arguments: {
|
||||
'asetId': aset.id,
|
||||
'isAset': true, // Add flag to indicate this is a single asset
|
||||
},
|
||||
preventDuplicates: false,
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to format numbers for display
|
||||
|
||||