5 Commits

Author SHA1 Message Date
47766bbdda kelola penyewa dan beberapa error fix 2025-07-09 16:01:10 +07:00
0423c2fdf9 semua fitur selesai 2025-06-30 15:22:38 +07:00
8284c93aa5 fitur petugas 2025-06-22 09:25:58 +07:00
c4dd4fdfa2 fitur order paket 2025-06-05 17:00:44 +07:00
046eac48e8 Merge pull request #1 from andreasmalvino/andreasmalvino-patch-1
Update order_sewa_paket_view.dart
2025-06-04 14:46:49 +07:00
137 changed files with 25118 additions and 8002 deletions

View File

@ -1,8 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application <application
android:label="bumrent_app" android:label="BumRent"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/launcher_icon">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/images/logo_app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -427,7 +427,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; 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_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@ -484,7 +484,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; 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_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Bumrent App</string> <string>BumRent</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>bumrent_app</string> <string>BumRent</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>

View File

@ -19,12 +19,12 @@ class PetugasBumdesBinding extends Bindings {
print('Error removing controller: $e'); print('Error removing controller: $e');
} }
// Gunakan put untuk memastikan controller selalu tersedia dan permanent // Gunakan lazyPut untuk memastikan controller hanya diinisialisasi saat dibutuhkan
Get.put<PetugasBumdesDashboardController>( Get.lazyPut<PetugasBumdesDashboardController>(
PetugasBumdesDashboardController(), () => PetugasBumdesDashboardController(),
permanent: true, fenix: true, // Akan dibuat ulang jika dihapus
); );
print('✅ PetugasBumdesDashboardController registered successfully'); print('✅ PetugasBumdesDashboardController initialized successfully');
} }
} }

View File

@ -5,7 +5,15 @@ import '../modules/warga/controllers/warga_dashboard_controller.dart';
class WargaBinding extends Bindings { class WargaBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
Get.lazyPut<AuthProvider>(() => AuthProvider()); // Pastikan AuthProvider teregistrasi
Get.lazyPut<WargaDashboardController>(() => WargaDashboardController()); 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
);
} }
} }

View File

@ -5,6 +5,7 @@ class AsetModel {
final String nama; final String nama;
final String deskripsi; final String deskripsi;
final String kategori; final String kategori;
final String jenis; // Add this line
final int harga; final int harga;
final int? denda; final int? denda;
final String status; final String status;
@ -14,17 +15,21 @@ class AsetModel {
final int? kuantitasTerpakai; final int? kuantitasTerpakai;
final String? satuanUkur; final String? satuanUkur;
// Untuk menampung URL gambar pertama dari tabel foto_aset // URL gambar utama (untuk backward compatibility)
String? imageUrl; String? imageUrl;
// List untuk menyimpan semua URL gambar aset
final RxList<String> imageUrls = <String>[].obs;
// Menggunakan RxList untuk membuatnya mutable dan reaktif // 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({ AsetModel({
required this.id, required this.id,
required this.nama, required this.nama,
required this.deskripsi, required this.deskripsi,
required this.kategori, required this.kategori,
this.jenis = 'Sewa', // Add this line with default value
required this.harga, required this.harga,
this.denda, this.denda,
required this.status, 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) { factory AsetModel.fromJson(Map<String, dynamic> json) {
return AsetModel( final model = AsetModel(
id: json['id'] ?? '', id: json['id'] ?? '',
nama: json['nama'] ?? '', nama: json['nama'] ?? '',
deskripsi: json['deskripsi'] ?? '', deskripsi: json['deskripsi'] ?? '',
kategori: json['kategori'] ?? '', kategori: json['kategori'] ?? '',
jenis: json['jenis'] ?? 'Sewa',
harga: json['harga'] ?? 0, harga: json['harga'] ?? 0,
denda: json['denda'], denda: json['denda'],
status: json['status'] ?? '', status: json['status'] ?? '',
createdAt: createdAt: json['created_at'] != null
json['created_at'] != null
? DateTime.parse(json['created_at']) ? DateTime.parse(json['created_at'])
: null, : null,
updatedAt: updatedAt: json['updated_at'] != null
json['updated_at'] != null
? DateTime.parse(json['updated_at']) ? DateTime.parse(json['updated_at'])
: null, : null,
kuantitas: json['kuantitas'], kuantitas: json['kuantitas'],
kuantitasTerpakai: json['kuantitas_terpakai'], kuantitasTerpakai: json['kuantitas_terpakai'],
satuanUkur: json['satuan_ukur'], 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() { Map<String, dynamic> toJson() {
return { final data = <String, dynamic>{
'id': id, 'id': id,
'nama': nama, 'nama': nama,
'deskripsi': deskripsi, 'deskripsi': deskripsi,
@ -80,5 +123,23 @@ class AsetModel {
'kuantitas_terpakai': kuantitasTerpakai, 'kuantitas_terpakai': kuantitasTerpakai,
'satuan_ukur': satuanUkur, '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;
} }
} }

View File

@ -1,54 +1,169 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer' as developer;
class PaketModel { class PaketModel {
final String? id; final String id;
final String? nama; final String nama;
final String? deskripsi; final String deskripsi;
final int? harga; final double harga;
final int? kuantitas; final int kuantitas;
final String? foto_paket; final String status;
final List<dynamic>? satuanWaktuSewa; 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({ PaketModel({
this.id, required this.id,
this.nama, required this.nama,
this.deskripsi, required this.deskripsi,
this.harga, required this.harga,
this.kuantitas, required this.kuantitas,
this.status = 'aktif',
required List<String> foto,
required List<Map<String, dynamic>> satuanWaktuSewa,
this.foto_paket, this.foto_paket,
this.satuanWaktuSewa, List<String>? images,
}); required this.createdAt,
required this.updatedAt,
}) : foto = List.from(foto),
satuanWaktuSewa = List.from(satuanWaktuSewa),
images = images != null ? List.from(images) : [];
Map<String, dynamic> toMap() { // Add copyWith method for immutability patterns
return { 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: 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,
);
}
// 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(),
);
}
// Convert to JSON
Map<String, dynamic> toJson() => {
'id': id, 'id': id,
'nama': nama, 'nama': nama,
'deskripsi': deskripsi, 'deskripsi': deskripsi,
'harga': harga, 'harga': harga,
'kuantitas': kuantitas, 'kuantitas': kuantitas,
'foto': foto,
'foto_paket': foto_paket, 'foto_paket': foto_paket,
'satuanWaktuSewa': satuanWaktuSewa, 'images': images,
'satuan_waktu_sewa': satuanWaktuSewa,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
}; };
}
factory PaketModel.fromMap(Map<String, dynamic> map) {
return PaketModel( // Get the first photo URL or a placeholder
id: map['id'], String get firstPhotoUrl => foto.isNotEmpty ? foto.first : '';
nama: map['nama'],
deskripsi: map['deskripsi'], // Get the formatted price
harga: map['harga']?.toInt(), String get formattedPrice => 'Rp${harga.toStringAsFixed(0).replaceAllMapped(
kuantitas: map['kuantitas']?.toInt(), RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
foto_paket: map['foto_paket'], (Match m) => '${m[1]}.',
satuanWaktuSewa: map['satuanWaktuSewa'], )}';
// 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) {
String toJson() => json.encode(toMap()); return 0.0;
}
factory PaketModel.fromJson(String source) => PaketModel.fromMap(json.decode(source));
@override
String toString() {
return 'PaketModel(id: $id, nama: $nama, deskripsi: $deskripsi, harga: $harga, kuantitas: $kuantitas, foto_paket: $foto_paket, satuanWaktuSewa: $satuanWaktuSewa)';
} }
} }

View 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),
);
}
}

View File

@ -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'],
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -75,6 +75,25 @@ class AuthProvider extends GetxService {
await client.auth.signOut(); 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; User? get currentUser => client.auth.currentUser;
Stream<AuthState> get authChanges => client.auth.onAuthStateChange; Stream<AuthState> get authChanges => client.auth.onAuthStateChange;
@ -415,28 +434,17 @@ class AuthProvider extends GetxService {
final userData = final userData =
await client await client
.from('warga_desa') .from('warga_desa')
.select('nomor_telepon, no_telepon, phone') .select('no_hp')
.eq('user_id', user.id) .eq('user_id', user.id)
.maybeSingle(); .maybeSingle();
// Jika berhasil mendapatkan data, cek beberapa kemungkinan nama kolom // Jika berhasil mendapatkan data, cek beberapa kemungkinan nama kolom
if (userData != null) { if (userData != null) {
if (userData.containsKey('nomor_telepon')) { if (userData.containsKey('no_hp')) {
final phone = userData['nomor_telepon']?.toString(); final phone = userData['no_hp']?.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 (phone != null && phone.isNotEmpty) return phone; if (phone != null && phone.isNotEmpty) return phone;
} }
} }
// Fallback ke data dari Supabase Auth // Fallback ke data dari Supabase Auth
final userMetadata = user.userMetadata; final userMetadata = user.userMetadata;
if (userMetadata != null) { 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) // Mendapatkan data sewa_aset berdasarkan status (misal: MENUNGGU PEMBAYARAN, PEMBAYARANAN DENDA)
Future<List<Map<String, dynamic>>> getSewaAsetByStatus( Future<List<Map<String, dynamic>>> getSewaAsetByStatus(
List<String> statuses, List<String> statuses,
@ -507,28 +655,97 @@ class AuthProvider extends GetxService {
} }
try { try {
debugPrint( 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 // Supabase expects the IN filter as a comma-separated string in parentheses
final statusString = '(${statuses.map((s) => '"$s"').join(',')})'; final statusString = '(${statuses.map((s) => '"$s"').join(',')})';
// Get sewa_aset records filtered by user_id and status
final response = await client final response = await client
.from('sewa_aset') .from('sewa_aset')
.select('*') .select('*')
.eq('user_id', user.id) .eq('user_id', user.id)
.filter('status', 'in', statusString); .filter('status', 'in', statusString)
debugPrint('Fetched sewa_aset count: \\${response.length}'); .order('created_at', ascending: false);
// Pastikan response adalah List
debugPrint('Fetched sewa_aset count: ${response.length}');
// Process the response to handle package data
if (response is List) { if (response is List) {
return response final List<Map<String, dynamic>> processedResponse = [];
.map<Map<String, dynamic>>(
(item) => Map<String, dynamic>.from(item), for (var item in response) {
) final Map<String, dynamic> processedItem = Map<String, dynamic>.from(
.toList(); 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 { } else {
return []; return [];
} }
} catch (e) { } catch (e) {
debugPrint('Error fetching sewa_aset by status: \\${e.toString()}'); debugPrint('Error fetching sewa_aset by status: ${e.toString()}');
return []; return [];
} }
} }

View File

@ -9,6 +9,16 @@ class PesananProvider {
final SupabaseClient _supabase = Supabase.instance.client; final SupabaseClient _supabase = Supabase.instance.client;
final _tableName = 'pesanan'; 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 { Future<List<PesananModel>> getPesananByUserId(String userId) async {
try { try {
final response = await _supabase final response = await _supabase

10
lib/app/main.dart Normal file
View 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());
}

View File

@ -2,12 +2,18 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../data/providers/auth_provider.dart'; import '../../../data/providers/auth_provider.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import 'dart:math';
import '../../../modules/warga/controllers/warga_dashboard_controller.dart';
class AuthController extends GetxController { class AuthController extends GetxController {
final AuthProvider _authProvider = Get.find<AuthProvider>(); final AuthProvider _authProvider = Get.find<AuthProvider>();
final emailController = TextEditingController(); final emailController = TextEditingController();
final passwordController = TextEditingController(); final passwordController = TextEditingController();
final formKey = GlobalKey<FormState>();
final nameController = TextEditingController();
final confirmPasswordController = TextEditingController();
final RxBool isConfirmPasswordVisible = false.obs;
// Form fields for registration // Form fields for registration
final RxString email = ''.obs; final RxString email = ''.obs;
@ -15,6 +21,11 @@ class AuthController extends GetxController {
final RxString nik = ''.obs; final RxString nik = ''.obs;
final RxString phoneNumber = ''.obs; final RxString phoneNumber = ''.obs;
final RxString selectedRole = 'WARGA'.obs; // Default role 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 // Form status
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
@ -28,6 +39,10 @@ class AuthController extends GetxController {
isPasswordVisible.value = !isPasswordVisible.value; isPasswordVisible.value = !isPasswordVisible.value;
} }
void toggleConfirmPasswordVisibility() {
isConfirmPasswordVisible.value = !isConfirmPasswordVisible.value;
}
// Change role selection // Change role selection
void setRole(String? role) { void setRole(String? role) {
if (role != null) { if (role != null) {
@ -87,7 +102,7 @@ class AuthController extends GetxController {
// Navigate based on role name // Navigate based on role name
if (roleName == null) { if (roleName == null) {
_navigateToWargaDashboard(); // Default to warga if role name not found await _checkWargaStatusAndNavigate(); // Default to warga if role name not found
return; return;
} }
@ -96,6 +111,9 @@ class AuthController extends GetxController {
_navigateToPetugasBumdesDashboard(); _navigateToPetugasBumdesDashboard();
break; break;
case 'WARGA': case 'WARGA':
// For WARGA role, check account status in warga_desa table
await _checkWargaStatusAndNavigate();
break;
default: default:
_navigateToWargaDashboard(); _navigateToWargaDashboard();
break; 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() { void _navigateToPetugasBumdesDashboard() {
Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD); Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
} }
void _navigateToWargaDashboard() { 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 { void forgotPassword() async {
@ -140,7 +218,7 @@ class AuthController extends GetxController {
Get.snackbar( Get.snackbar(
'Berhasil', 'Berhasil',
'Link reset password telah dikirim ke email Anda', 'Link reset password telah dikirim ke email Anda',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green[100], backgroundColor: Colors.green[100],
colorText: Colors.green[800], colorText: Colors.green[800],
icon: const Icon(Icons.check_circle, color: Colors.green), icon: const Icon(Icons.check_circle, color: Colors.green),
@ -172,63 +250,87 @@ class AuthController extends GetxController {
void onClose() { void onClose() {
emailController.dispose(); emailController.dispose();
passwordController.dispose(); passwordController.dispose();
nameController.dispose();
confirmPasswordController.dispose();
super.onClose(); super.onClose();
} }
// Register user implementation // Register user implementation
Future<void> registerUser() async { Future<void> registerUser() async {
// Validate all required fields // Clear previous error messages
if (email.value.isEmpty || errorMessage.value = '';
password.value.isEmpty ||
nik.value.isEmpty || // Validate form fields
phoneNumber.value.isEmpty) { if (!formKey.currentState!.validate()) {
errorMessage.value = 'Semua field harus diisi';
return; return;
} }
// Basic validation for email // Validate date of birth separately (since it's not a standard form field)
if (!GetUtils.isEmail(email.value.trim())) { if (!validateDateOfBirth()) {
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)';
return; return;
} }
try { try {
isLoading.value = true; isLoading.value = true;
errorMessage.value = '';
// Create user with Supabase // Format tanggal lahir to string (YYYY-MM-DD)
final response = await _authProvider.signUp( 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(), email: email.value.trim(),
password: password.value, password: password.value,
data: { data: {
'nik': nik.value.trim(), 'role_id':
'phone_number': phoneNumber.value.trim(), 'bb5360d5-8fd0-404e-8f6f-71ec4d8ad0ae', // Fixed role_id for WARGA
'role': selectedRole.value,
}, },
); );
// Check if registration was successful
if (response.user != null) { if (response.user != null) {
// Registration successful // 2. Get the UID from the created auth user
Get.offNamed(Routes.REGISTRATION_SUCCESS); 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 { } else {
errorMessage.value = 'Gagal mendaftar. Silakan coba lagi.'; errorMessage.value = 'Gagal mendaftar. Silakan coba lagi.';
} }
@ -239,4 +341,250 @@ class AuthController extends GetxController {
isLoading.value = false; 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');
}
}
} }

View File

@ -26,12 +26,8 @@ class ForgotPasswordView extends GetView<AuthController> {
Opacity( Opacity(
opacity: 0.03, opacity: 0.03,
child: Container( child: Container(
decoration: const BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( color: Colors.blue[50], // Temporary solid color
image: AssetImage('assets/images/pattern.png'),
repeat: ImageRepeat.repeat,
scale: 4.0,
),
), ),
), ),
), ),

View File

@ -9,6 +9,7 @@ class LoginView extends GetView<AuthController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false,
body: Stack( body: Stack(
children: [ children: [
// Background gradient // Background gradient
@ -30,12 +31,8 @@ class LoginView extends GetView<AuthController> {
Opacity( Opacity(
opacity: 0.03, opacity: 0.03,
child: Container( child: Container(
decoration: const BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( color: Colors.blue[50], // Temporary solid color
image: AssetImage('assets/images/pattern.png'),
repeat: ImageRepeat.repeat,
scale: 4.0,
),
), ),
), ),
), ),
@ -76,20 +73,19 @@ class LoginView extends GetView<AuthController> {
), ),
), ),
// Main content // Main content with keyboard avoidance
SafeArea( SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const BouncingScrollPhysics(), physics: const ClampingScrollPhysics(),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0), padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 50), SizedBox(height: MediaQuery.of(context).size.height * 0.05),
_buildHeader(), _buildHeader(),
const SizedBox(height: 40), SizedBox(height: MediaQuery.of(context).size.height * 0.03),
_buildLoginCard(), _buildLoginCard(),
const SizedBox(height: 24),
_buildRegisterLink(), _buildRegisterLink(),
const SizedBox(height: 30), const SizedBox(height: 30),
], ],
@ -108,12 +104,12 @@ class LoginView extends GetView<AuthController> {
tag: 'logo', tag: 'logo',
child: Image.asset( child: Image.asset(
'assets/images/logo.png', 'assets/images/logo.png',
width: 220, width: 180,
height: 220, height: 180,
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
return Icon( return Icon(
Icons.apartment_rounded, Icons.apartment_rounded,
size: 180, size: 150,
color: AppColors.primary, color: AppColors.primary,
); );
}, },
@ -128,7 +124,7 @@ class LoginView extends GetView<AuthController> {
shadowColor: AppColors.shadow, shadowColor: AppColors.shadow,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(28.0), padding: const EdgeInsets.all(24.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -150,7 +146,7 @@ class LoginView extends GetView<AuthController> {
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 24),
// Email field // Email field
_buildInputLabel('Email'), _buildInputLabel('Email'),
@ -161,7 +157,7 @@ class LoginView extends GetView<AuthController> {
prefixIcon: Icons.email_outlined, prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
), ),
const SizedBox(height: 24), const SizedBox(height: 12),
// Password field // Password field
_buildInputLabel('Password'), _buildInputLabel('Password'),
@ -204,13 +200,12 @@ class LoginView extends GetView<AuthController> {
), ),
), ),
), ),
const SizedBox(height: 32),
// Login button // Login button
Obx( Obx(
() => SizedBox( () => SizedBox(
width: double.infinity, width: double.infinity,
height: 56, height: 50, // Slightly smaller height
child: ElevatedButton( child: ElevatedButton(
onPressed: onPressed:
controller.isLoading.value ? null : controller.login, controller.isLoading.value ? null : controller.login,
@ -315,6 +310,16 @@ class LoginView extends GetView<AuthController> {
keyboardType: keyboardType, keyboardType: keyboardType,
obscureText: obscureText, obscureText: obscureText,
style: TextStyle(fontSize: 16, color: AppColors.textPrimary), 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( decoration: InputDecoration(
hintText: hintText, hintText: hintText,
hintStyle: TextStyle(color: AppColors.textLight), hintStyle: TextStyle(color: AppColors.textLight),

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../theme/app_colors.dart'; import '../../../theme/app_colors.dart';
import 'package:flutter/rendering.dart';
import '../../../routes/app_routes.dart';
class RegistrationSuccessView extends StatefulWidget { class RegistrationSuccessView extends StatefulWidget {
const RegistrationSuccessView({Key? key}) : super(key: key); const RegistrationSuccessView({Key? key}) : super(key: key);
@ -15,10 +18,17 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
late AnimationController _animationController; late AnimationController _animationController;
late Animation<double> _scaleAnimation; late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation; late Animation<double> _fadeAnimation;
String? registerId;
@override @override
void initState() { void initState() {
super.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( _animationController = AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: 1000), duration: const Duration(milliseconds: 1000),
@ -215,7 +225,7 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text( 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( style: TextStyle(
fontSize: 16, fontSize: 16,
color: AppColors.textSecondary, color: AppColors.textSecondary,
@ -224,6 +234,84 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
textAlign: TextAlign.center, 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( child: ElevatedButton(
onPressed: () { onPressed: () {
// Navigate back to login page // Navigate back to login page
Get.offAllNamed('/login'); Get.offNamed(Routes.LOGIN);
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary, backgroundColor: AppColors.primary,

File diff suppressed because it is too large Load Diff

View File

@ -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());
}
}

View File

@ -1,6 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/petugas_aset_controller.dart'; import '../controllers/petugas_aset_controller.dart';
import '../controllers/petugas_bumdes_dashboard_controller.dart'; import '../controllers/petugas_bumdes_dashboard_controller.dart';
import '../../../data/providers/aset_provider.dart';
class PetugasAsetBinding extends Bindings { class PetugasAsetBinding extends Bindings {
@override @override
@ -10,6 +11,7 @@ class PetugasAsetBinding extends Bindings {
Get.put(PetugasBumdesDashboardController(), permanent: true); Get.put(PetugasBumdesDashboardController(), permanent: true);
} }
Get.lazyPut<AsetProvider>(() => AsetProvider());
Get.lazyPut<PetugasAsetController>(() => PetugasAsetController()); Get.lazyPut<PetugasAsetController>(() => PetugasAsetController());
} }
} }

View File

@ -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(),
);
}
}

View File

@ -1,9 +1,14 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/petugas_sewa_controller.dart'; import '../controllers/petugas_sewa_controller.dart';
import '../../../data/providers/aset_provider.dart';
class PetugasDetailSewaBinding extends Bindings { class PetugasDetailSewaBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
// Ensure AsetProvider is registered
if (!Get.isRegistered<AsetProvider>()) {
Get.put(AsetProvider(), permanent: true);
}
// Memastikan controller sudah tersedia // Memastikan controller sudah tersedia
Get.lazyPut<PetugasSewaController>( Get.lazyPut<PetugasSewaController>(
() => PetugasSewaController(), () => PetugasSewaController(),

View File

@ -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());
}
}

View File

@ -1,15 +1,25 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
import '../controllers/petugas_paket_controller.dart'; import '../controllers/petugas_paket_controller.dart';
import '../controllers/petugas_bumdes_dashboard_controller.dart'; import '../controllers/petugas_bumdes_dashboard_controller.dart';
class PetugasPaketBinding extends Bindings { class PetugasPaketBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
// Register AsetProvider first
if (!Get.isRegistered<AsetProvider>()) {
Get.put(AsetProvider(), permanent: true);
}
// Ensure dashboard controller is registered // Ensure dashboard controller is registered
if (!Get.isRegistered<PetugasBumdesDashboardController>()) { if (!Get.isRegistered<PetugasBumdesDashboardController>()) {
Get.put(PetugasBumdesDashboardController(), permanent: true); Get.put(PetugasBumdesDashboardController(), permanent: true);
} }
Get.lazyPut<PetugasPaketController>(() => PetugasPaketController()); // Register the controller
Get.lazyPut<PetugasPaketController>(
() => PetugasPaketController(),
fenix: true,
);
} }
} }

View File

@ -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,
);
}
}

View File

@ -1,9 +1,14 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/petugas_sewa_controller.dart'; import '../controllers/petugas_sewa_controller.dart';
import '../../../data/providers/aset_provider.dart';
class PetugasSewaBinding extends Bindings { class PetugasSewaBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
// Ensure AsetProvider is registered
if (!Get.isRegistered<AsetProvider>()) {
Get.put(AsetProvider(), permanent: true);
}
Get.lazyPut<PetugasSewaController>(() => PetugasSewaController()); Get.lazyPut<PetugasSewaController>(() => PetugasSewaController());
} }
} }

View File

@ -49,7 +49,7 @@ class ListPetugasMitraController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Petugas mitra berhasil ditambahkan', 'Petugas mitra berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -62,7 +62,7 @@ class ListPetugasMitraController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Data petugas mitra berhasil diperbarui', 'Data petugas mitra berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }
@ -73,7 +73,7 @@ class ListPetugasMitraController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Petugas mitra berhasil dihapus', 'Petugas mitra berhasil dihapus',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -86,7 +86,7 @@ class ListPetugasMitraController extends GetxController {
Get.snackbar( Get.snackbar(
'Status Diperbarui', 'Status Diperbarui',
'Status petugas mitra diubah menjadi ${!currentStatus ? 'Aktif' : 'Nonaktif'}', 'Status petugas mitra diubah menjadi ${!currentStatus ? 'Aktif' : 'Nonaktif'}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }

View File

@ -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;
}
}
}

View File

@ -1,6 +1,11 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../data/providers/aset_provider.dart';
import '../../../data/models/aset_model.dart';
class PetugasAsetController extends GetxController { class PetugasAsetController extends GetxController {
final AsetProvider _asetProvider = Get.find<AsetProvider>();
// Observable lists for asset data // Observable lists for asset data
final asetList = <Map<String, dynamic>>[].obs; final asetList = <Map<String, dynamic>>[].obs;
final filteredAsetList = <Map<String, dynamic>>[].obs; final filteredAsetList = <Map<String, dynamic>>[].obs;
@ -27,95 +32,100 @@ class PetugasAsetController extends GetxController {
loadAsetData(); loadAsetData();
} }
// Load sample asset data (would be replaced with API call in production) // Load asset data from AsetProvider
Future<void> loadAsetData() async { Future<void> loadAsetData() async {
isLoading.value = true;
try { try {
// Simulate API call with a delay isLoading.value = true;
await Future.delayed(const Duration(seconds: 1)); debugPrint('PetugasAsetController: Starting to load asset data...');
// Sample assets data // Fetch data using AsetProvider
final sampleData = [ final asetData = await _asetProvider.getSewaAsets();
{ debugPrint(
'id': '1', 'PetugasAsetController: Fetched ${asetData.length} assets from Supabase',
'nama': 'Meja Rapat', );
'kategori': 'Furniture',
'jenis': 'Sewa', // Added jenis field
'harga': 50000,
'satuan': 'per hari',
'stok': 10,
'deskripsi':
'Meja rapat kayu jati ukuran besar untuk acara pertemuan',
'gambar': 'https://example.com/meja.jpg',
'tersedia': true,
},
{
'id': '2',
'nama': 'Kursi Taman',
'kategori': 'Furniture',
'jenis': 'Sewa', // Added jenis field
'harga': 10000,
'satuan': 'per hari',
'stok': 50,
'deskripsi': 'Kursi taman plastik yang nyaman untuk acara outdoor',
'gambar': 'https://example.com/kursi.jpg',
'tersedia': true,
},
{
'id': '3',
'nama': 'Proyektor',
'kategori': 'Elektronik',
'jenis': 'Sewa', // Added jenis field
'harga': 100000,
'satuan': 'per hari',
'stok': 5,
'deskripsi': 'Proyektor HD dengan brightness tinggi',
'gambar': 'https://example.com/proyektor.jpg',
'tersedia': true,
},
{
'id': '4',
'nama': 'Sound System',
'kategori': 'Elektronik',
'jenis': 'Langganan', // Added jenis field
'harga': 200000,
'satuan': 'per bulan',
'stok': 3,
'deskripsi': 'Sound system lengkap dengan speaker dan mixer',
'gambar': 'https://example.com/sound.jpg',
'tersedia': false,
},
{
'id': '5',
'nama': 'Mobil Pick Up',
'kategori': 'Kendaraan',
'jenis': 'Langganan', // Added jenis field
'harga': 250000,
'satuan': 'per bulan',
'stok': 2,
'deskripsi': 'Mobil pick up untuk mengangkut barang',
'gambar': 'https://example.com/pickup.jpg',
'tersedia': true,
},
{
'id': '6',
'nama': 'Internet Fiber',
'kategori': 'Elektronik',
'jenis': 'Langganan', // Added jenis field
'harga': 350000,
'satuan': 'per bulan',
'stok': 15,
'deskripsi': 'Paket internet fiber 100Mbps untuk kantor',
'gambar': 'https://example.com/internet.jpg',
'tersedia': true,
},
];
asetList.assignAll(sampleData); if (asetData.isEmpty) {
applyFilters(); // Apply default filters debugPrint('PetugasAsetController: No assets found in Supabase');
} catch (e) { }
print('Error loading asset data: $e');
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 { } finally {
isLoading.value = false; isLoading.value = false;
} }
@ -170,8 +180,10 @@ class PetugasAsetController extends GetxController {
} }
// Change tab (Sewa or Langganan) // Change tab (Sewa or Langganan)
void changeTab(int index) { Future<void> changeTab(int index) async {
selectedTabIndex.value = index; selectedTabIndex.value = index;
// Reload data when changing tabs to ensure we have the correct data for the selected tab
await loadAsetData();
applyFilters(); applyFilters();
} }
@ -210,8 +222,63 @@ class PetugasAsetController extends GetxController {
} }
// Delete an asset // Delete an asset
void deleteAset(String id) { 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); asetList.removeWhere((aset) => aset['id'] == id);
// Apply filters to update the UI
applyFilters(); 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;
}
} }
} }

View File

@ -66,7 +66,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memuat data. Silakan coba lagi.', 'Gagal memuat data. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} finally { } finally {
isLoading.value = false; isLoading.value = false;
@ -92,7 +92,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Rekening Utama', 'Rekening Utama',
'Rekening ${account['bank_name']} telah dijadikan 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( Get.snackbar(
'Rekening Ditambahkan', 'Rekening Ditambahkan',
'Rekening bank baru telah berhasil ditambahkan', 'Rekening bank baru telah berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -125,7 +125,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Rekening Diperbarui', 'Rekening Diperbarui',
'Informasi rekening bank telah berhasil diperbarui', 'Informasi rekening bank telah berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }
@ -138,7 +138,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Tidak Dapat Menghapus', 'Tidak Dapat Menghapus',
'Rekening utama tidak dapat dihapus. Silakan atur rekening lain sebagai utama terlebih dahulu.', 'Rekening utama tidak dapat dihapus. Silakan atur rekening lain sebagai utama terlebih dahulu.',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
return; return;
} }
@ -148,7 +148,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Rekening Dihapus', 'Rekening Dihapus',
'Rekening bank telah berhasil dihapus', 'Rekening bank telah berhasil dihapus',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }
@ -164,7 +164,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Status Diperbarui', 'Status Diperbarui',
'Status mitra telah diubah menjadi ${partner['is_active'] ? 'Aktif' : 'Tidak Aktif'}', '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( Get.snackbar(
'Mitra Ditambahkan', 'Mitra Ditambahkan',
'Mitra baru telah berhasil ditambahkan', 'Mitra baru telah berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -197,7 +197,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Mitra Diperbarui', 'Mitra Diperbarui',
'Informasi mitra telah berhasil diperbarui', 'Informasi mitra telah berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }
@ -210,7 +210,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Mitra Dihapus', 'Mitra Dihapus',
'Mitra telah berhasil dihapus', 'Mitra telah berhasil dihapus',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }

View File

@ -1,6 +1,14 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../data/providers/auth_provider.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import '../../../routes/app_routes.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 { class PetugasBumdesDashboardController extends GetxController {
AuthProvider? _authProvider; AuthProvider? _authProvider;
@ -8,40 +16,61 @@ class PetugasBumdesDashboardController extends GetxController {
// Reactive variables // Reactive variables
final userEmail = ''.obs; final userEmail = ''.obs;
final currentTabIndex = 0.obs; final currentTabIndex = 0.obs;
final avatarUrl = ''.obs;
final userName = ''.obs;
// Revenue Statistics // Revenue Statistics
final totalPendapatanBulanIni = 'Rp 8.500.000'.obs; final totalPendapatanBulanIni = ''.obs;
final totalPendapatanBulanLalu = 'Rp 7.200.000'.obs; final totalPendapatanBulanLalu = ''.obs;
final persentaseKenaikan = '18%'.obs; final persentaseKenaikan = ''.obs;
final isKenaikanPositif = true.obs; final isKenaikanPositif = true.obs;
// Revenue by Category // Revenue by Category
final pendapatanSewa = 'Rp 5.200.000'.obs; final pendapatanSewa = ''.obs;
final persentaseSewa = 100.obs; final persentaseSewa = 0.obs;
// Revenue Trends (last 6 months) // 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 // Status Counters for Sewa Aset
final terlaksanaCount = 5.obs; final terlaksanaCount = 0.obs;
final dijadwalkanCount = 1.obs; final dijadwalkanCount = 0.obs;
final aktifCount = 1.obs; final aktifCount = 0.obs;
final dibatalkanCount = 3.obs; final dibatalkanCount = 0.obs;
// Additional Sewa Aset Status Counters // Additional Sewa Aset Status Counters
final menungguPembayaranCount = 2.obs; final menungguPembayaranCount = 0.obs;
final periksaPembayaranCount = 1.obs; final periksaPembayaranCount = 0.obs;
final diterimaCount = 3.obs; final diterimaCount = 0.obs;
final pembayaranDendaCount = 1.obs; final pembayaranDendaCount = 0.obs;
final periksaPembayaranDendaCount = 0.obs; final periksaPembayaranDendaCount = 0.obs;
final selesaiCount = 4.obs; final selesaiCount = 0.obs;
// Status counts for Sewa // Status counts for Sewa
final pengajuanSewaCount = 5.obs; final pengajuanSewaCount = 0.obs;
final pemasanganCountSewa = 3.obs; final pemasanganCountSewa = 0.obs;
final sewaAktifCount = 10.obs; final sewaAktifCount = 0.obs;
final tagihanAktifCountSewa = 7.obs; final tagihanAktifCountSewa = 0.obs;
final periksaPembayaranCountSewa = 2.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 @override
void onInit() { void onInit() {
@ -49,36 +78,160 @@ class PetugasBumdesDashboardController extends GetxController {
try { try {
_authProvider = Get.find<AuthProvider>(); _authProvider = Get.find<AuthProvider>();
userEmail.value = _authProvider?.currentUser?.email ?? 'Tidak ada email'; userEmail.value = _authProvider?.currentUser?.email ?? 'Tidak ada email';
fetchPetugasAvatar();
fetchPetugasName();
} catch (e) { } catch (e) {
print('Error finding AuthProvider: $e'); print('Error finding AuthProvider: $e');
userEmail.value = 'Tidak ada email'; userEmail.value = 'Tidak ada email';
} }
print('\u2705 PetugasBumdesDashboardController initialized successfully');
// In a real app, these counts would be fetched from backend countSewaByStatus();
// loadStatusCounts(); fetchPembayaranStats();
print('✅ PetugasBumdesDashboardController initialized successfully'); fetchPenyewaStats();
} }
// Method to load status counts from backend Future<void> countSewaByStatus() async {
// Future<void> loadStatusCounts() async { try {
// try { final data = await SewaService().fetchAllSewa();
// final response = await _asetProvider.getSewaStatusCounts(); menungguPembayaranCount.value =
// if (response != null) { data.where((s) => s.status == 'MENUNGGU PEMBAYARAN').length;
// terlaksanaCount.value = response['terlaksana'] ?? 0; periksaPembayaranCount.value =
// dijadwalkanCount.value = response['dijadwalkan'] ?? 0; data.where((s) => s.status == 'PERIKSA PEMBAYARAN').length;
// aktifCount.value = response['aktif'] ?? 0; diterimaCount.value = data.where((s) => s.status == 'DITERIMA').length;
// dibatalkanCount.value = response['dibatalkan'] ?? 0; pembayaranDendaCount.value =
// menungguPembayaranCount.value = response['menunggu_pembayaran'] ?? 0; data.where((s) => s.status == 'PEMBAYARAN DENDA').length;
// periksaPembayaranCount.value = response['periksa_pembayaran'] ?? 0; periksaPembayaranDendaCount.value =
// diterimaCount.value = response['diterima'] ?? 0; data.where((s) => s.status == 'PERIKSA PEMBAYARAN DENDA').length;
// pembayaranDendaCount.value = response['pembayaran_denda'] ?? 0; selesaiCount.value = data.where((s) => s.status == 'SELESAI').length;
// periksaPembayaranDendaCount.value = response['periksa_pembayaran_denda'] ?? 0; print(
// selesaiCount.value = response['selesai'] ?? 0; 'Count for MENUNGGU PEMBAYARAN: \\${menungguPembayaranCount.value}',
// } );
// } catch (e) { print('Count for PERIKSA PEMBAYARAN: \\${periksaPembayaranCount.value}');
// print('Error loading status counts: $e'); 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) { void changeTab(int index) {
try { try {
@ -102,6 +255,10 @@ class PetugasBumdesDashboardController extends GetxController {
// Navigate to Sewa page // Navigate to Sewa page
navigateToSewa(); navigateToSewa();
break; break;
case 4:
// Navigate to Penyewa page
navigateToPenyewa();
break;
} }
} catch (e) { } catch (e) {
print('Error changing tab: $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 { void logout() async {
try { try {
// Store login route for navigation
final loginRoute = Routes.LOGIN;
// Sign out from Supabase
if (_authProvider != null) { if (_authProvider != null) {
await _authProvider!.signOut(); 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) { } catch (e) {
print('Error during logout: $e'); print('Error during logout: $e');
// Still try to navigate to login even if sign out fails // Still try to navigate to login even if sign out fails
Get.offAllNamed(Routes.LOGIN); 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
}
} }

View File

@ -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';
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -79,7 +79,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Rekening utama berhasil diubah', 'Rekening utama berhasil diubah',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -93,7 +93,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Status mitra berhasil diubah', 'Status mitra berhasil diubah',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -110,7 +110,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Rekening bank berhasil ditambahkan', 'Rekening bank berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -124,7 +124,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Rekening bank berhasil diperbarui', 'Rekening bank berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -145,7 +145,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Rekening bank berhasil dihapus', 'Rekening bank berhasil dihapus',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -155,7 +155,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Mitra berhasil ditambahkan', 'Mitra berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -166,7 +166,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Mitra berhasil diperbarui', 'Mitra berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -177,7 +177,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Mitra berhasil dihapus', 'Mitra berhasil dihapus',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }

View File

@ -1,24 +1,24 @@
import 'package:flutter/material.dart';
import 'package:get/get.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 { class PetugasPaketController extends GetxController {
final isLoading = false.obs; // Dependencies
final searchQuery = ''.obs; final AsetProvider _asetProvider = Get.find<AsetProvider>();
final selectedCategory = 'Semua'.obs;
final sortBy = 'Terbaru'.obs;
// Kategori untuk filter // State
final categories = <String>[ final RxBool isLoading = false.obs;
'Semua', final RxString searchQuery = ''.obs;
'Pesta', final RxString selectedCategory = 'Semua'.obs;
'Rapat', final RxString sortBy = 'Terbaru'.obs;
'Olahraga', final RxList<PaketModel> packages = <PaketModel>[].obs;
'Pernikahan', final RxList<PaketModel> filteredPackages = <PaketModel>[].obs;
'Lainnya',
];
// Opsi pengurutan // Sort options for the dropdown
final sortOptions = <String>[ final List<String> sortOptions = [
'Terbaru', 'Terbaru',
'Terlama', 'Terlama',
'Harga Tertinggi', 'Harga Tertinggi',
@ -27,172 +27,257 @@ class PetugasPaketController extends GetxController {
'Nama Z-A', 'Nama Z-A',
]; ];
// Data dummy paket // For backward compatibility
final paketList = <Map<String, dynamic>>[].obs; final RxList<Map<String, dynamic>> paketList = <Map<String, dynamic>>[].obs;
final filteredPaketList = <Map<String, dynamic>>[].obs; final RxList<Map<String, dynamic>> filteredPaketList =
<Map<String, dynamic>>[].obs;
// Logger
late final Logger _logger;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
loadPaketData();
}
// Format harga ke Rupiah // Initialize logger
String formatPrice(int price) { _logger = Logger(
final formatter = NumberFormat.currency( printer: PrettyPrinter(
locale: 'id', methodCount: 0,
symbol: 'Rp ', errorMethodCount: 5,
decimalDigits: 0, colors: true,
printEmojis: true,
),
); );
return formatter.format(price);
// Load initial data
fetchPackages();
} }
// Load data paket dummy /// Fetch packages from the API
Future<void> loadPaketData() async { Future<void> fetchPackages() async {
try {
isLoading.value = true; isLoading.value = true;
await Future.delayed(const Duration(milliseconds: 800)); // Simulasi loading _logger.i('🔄 [fetchPackages] Fetching packages...');
paketList.value = [ final result = await _asetProvider.getAllPaket();
{
'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(); 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; isLoading.value = false;
} }
}
// Filter paket berdasarkan search query dan kategori /// Update legacy paketList for backward compatibility
void _updateLegacyPaketList() {
try {
_logger.d('🔄 [_updateLegacyPaketList] Updating legacy paketList...');
final List<Map<String, dynamic>> legacyList =
packages.map((pkg) {
return {
'id': pkg.id,
'nama': pkg.nama,
'deskripsi': pkg.deskripsi,
'harga': pkg.harga,
'kuantitas': pkg.kuantitas,
'status': pkg.status, // Add status to legacy mapping
'foto': pkg.foto,
'foto_paket': pkg.foto_paket,
'images': pkg.images,
'satuanWaktuSewa': pkg.satuanWaktuSewa,
'created_at': pkg.createdAt,
'updated_at': pkg.updatedAt,
};
}).toList();
paketList.assignAll(legacyList);
filteredPaketList.assignAll(legacyList);
_logger.d(
'✅ [_updateLegacyPaketList] Updated ${legacyList.length} packages',
);
} catch (e, stackTrace) {
_logger.e(
'❌ [_updateLegacyPaketList] Error updating legacy list',
error: e,
stackTrace: stackTrace,
);
}
}
/// For backward compatibility
Future<void> loadPaketData() async {
_logger.d(' [loadPaketData] Using fetchPackages() instead');
await fetchPackages();
}
/// Filter packages based on search query and category
void filterPaket() { void filterPaket() {
filteredPaketList.value = try {
paketList.where((paket) { _logger.d('🔄 [filterPaket] Filtering packages...');
final matchesQuery =
paket['nama'].toString().toLowerCase().contains( if (searchQuery.value.isEmpty && selectedCategory.value == 'Semua') {
searchQuery.value.toLowerCase(), filteredPackages.value = List.from(packages);
) || filteredPaketList.value = List.from(paketList);
paket['deskripsi'].toString().toLowerCase().contains( } else {
// Filter new packages
filteredPackages.value =
packages.where((paket) {
final matchesSearch =
searchQuery.value.isEmpty ||
paket.nama.toLowerCase().contains(
searchQuery.value.toLowerCase(), searchQuery.value.toLowerCase(),
); );
final matchesCategory = // For now, we're not using categories in the new model
selectedCategory.value == 'Semua' || // You can add category filtering if needed
paket['kategori'] == selectedCategory.value; final matchesCategory = selectedCategory.value == 'Semua';
return matchesQuery && matchesCategory; return matchesSearch && matchesCategory;
}).toList(); }).toList();
// Sort the filtered list // Also update legacy list for backward compatibility
sortFilteredList(); 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();
} }
// Sort the filtered list sortFilteredList();
_logger.d(
'✅ [filterPaket] Filtered to ${filteredPackages.length} packages',
);
} catch (e, stackTrace) {
_logger.e(
'❌ [filterPaket] Error filtering packages',
error: e,
stackTrace: stackTrace,
);
}
}
/// Sort the filtered list based on the selected sort option
void sortFilteredList() { void sortFilteredList() {
try {
_logger.d('🔄 [sortFilteredList] Sorting packages by ${sortBy.value}');
// Sort new packages
switch (sortBy.value) {
case 'Terbaru':
filteredPackages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
break;
case 'Terlama':
filteredPackages.sort((a, b) => a.createdAt.compareTo(b.createdAt));
break;
case 'Harga Tertinggi':
filteredPackages.sort((a, b) => b.harga.compareTo(a.harga));
break;
case 'Harga Terendah':
filteredPackages.sort((a, b) => a.harga.compareTo(b.harga));
break;
case 'Nama A-Z':
filteredPackages.sort((a, b) => a.nama.compareTo(b.nama));
break;
case 'Nama Z-A':
filteredPackages.sort((a, b) => b.nama.compareTo(a.nama));
break;
}
// Also sort legacy list for backward compatibility
switch (sortBy.value) { switch (sortBy.value) {
case 'Terbaru': case 'Terbaru':
filteredPaketList.sort( filteredPaketList.sort(
(a, b) => b['created_at'].compareTo(a['created_at']), (a, b) => ((b['created_at'] ?? '') as String).compareTo(
(a['created_at'] ?? '') as String,
),
); );
break; break;
case 'Terlama': case 'Terlama':
filteredPaketList.sort( filteredPaketList.sort(
(a, b) => a['created_at'].compareTo(b['created_at']), (a, b) => ((a['created_at'] ?? '') as String).compareTo(
(b['created_at'] ?? '') as String,
),
); );
break; break;
case 'Harga Tertinggi': case 'Harga Tertinggi':
filteredPaketList.sort((a, b) => b['harga'].compareTo(a['harga'])); filteredPaketList.sort(
(a, b) =>
((b['harga'] ?? 0) as int).compareTo((a['harga'] ?? 0) as int),
);
break; break;
case 'Harga Terendah': case 'Harga Terendah':
filteredPaketList.sort((a, b) => a['harga'].compareTo(b['harga'])); filteredPaketList.sort(
(a, b) =>
((a['harga'] ?? 0) as int).compareTo((b['harga'] ?? 0) as int),
);
break; break;
case 'Nama A-Z': case 'Nama A-Z':
filteredPaketList.sort((a, b) => a['nama'].compareTo(b['nama'])); filteredPaketList.sort(
(a, b) => ((a['nama'] ?? '') as String).compareTo(
(b['nama'] ?? '') as String,
),
);
break; break;
case 'Nama Z-A': case 'Nama Z-A':
filteredPaketList.sort((a, b) => b['nama'].compareTo(a['nama'])); filteredPaketList.sort(
(a, b) => ((b['nama'] ?? '') as String).compareTo(
(a['nama'] ?? '') as String,
),
);
break; break;
} }
_logger.d(
'✅ [sortFilteredList] Sorted ${filteredPackages.length} packages',
);
} catch (e, stackTrace) {
_logger.e(
'❌ [sortFilteredList] Error sorting packages',
error: e,
stackTrace: stackTrace,
);
}
} }
// Set search query dan filter paket // Set search query dan filter paket
@ -214,40 +299,179 @@ class PetugasPaketController extends GetxController {
} }
// Tambah paket baru // Tambah paket baru
void addPaket(Map<String, dynamic> paket) { Future<void> addPaket(Map<String, dynamic> paketData) async {
paketList.add(paket); try {
isLoading.value = true;
// Convert to PaketModel
final newPaket = PaketModel.fromJson({
...paketData,
'id': DateTime.now().millisecondsSinceEpoch.toString(),
'created_at': DateTime.now().toIso8601String(),
'updated_at': DateTime.now().toIso8601String(),
});
// Add to the list
packages.add(newPaket);
_updateLegacyPaketList();
filterPaket(); filterPaket();
Get.back(); Get.back();
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Paket baru berhasil ditambahkan', 'Paket baru berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM, 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 // Edit paket
void editPaket(String id, Map<String, dynamic> updatedPaket) { Future<void> editPaket(String id, Map<String, dynamic> updatedData) async {
final index = paketList.indexWhere((element) => element['id'] == id); try {
isLoading.value = true;
final index = packages.indexWhere((pkg) => pkg.id == id);
if (index >= 0) { if (index >= 0) {
paketList[index] = updatedPaket; // 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(); filterPaket();
Get.back(); Get.back();
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Paket berhasil diperbarui', 'Paket berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, 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 // Hapus paket
void deletePaket(String id) { Future<void> deletePaket(String id) async {
paketList.removeWhere((element) => element['id'] == id); 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(); filterPaket();
// Show success message
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Paket berhasil dihapus', 'Paket berhasil dihapus dari sistem',
snackPosition: SnackPosition.BOTTOM, 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)}';
}
}

View File

@ -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;
}
}
}

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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 { class PetugasSewaController extends GetxController {
// Reactive variables // Reactive variables
@ -7,7 +10,7 @@ class PetugasSewaController extends GetxController {
final searchQuery = ''.obs; final searchQuery = ''.obs;
final orderIdQuery = ''.obs; final orderIdQuery = ''.obs;
final selectedStatusFilter = 'Semua'.obs; final selectedStatusFilter = 'Semua'.obs;
final filteredSewaList = <Map<String, dynamic>>[].obs; final filteredSewaList = <SewaModel>[].obs;
// Filter options // Filter options
final List<String> statusFilters = [ final List<String> statusFilters = [
@ -15,13 +18,19 @@ class PetugasSewaController extends GetxController {
'Menunggu Pembayaran', 'Menunggu Pembayaran',
'Periksa Pembayaran', 'Periksa Pembayaran',
'Diterima', 'Diterima',
'Aktif',
'Dikembalikan', 'Dikembalikan',
'Selesai', 'Selesai',
'Dibatalkan', 'Dibatalkan',
]; ];
// Mock data for sewa list // 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 @override
void onInit() { void onInit() {
@ -41,26 +50,24 @@ class PetugasSewaController extends GetxController {
void _updateFilteredList() { void _updateFilteredList() {
filteredSewaList.value = filteredSewaList.value =
sewaList.where((sewa) { sewaList.where((sewa) {
// Apply search filter final query = searchQuery.value.toLowerCase();
final matchesSearch = sewa['nama_warga'] // Apply search filter: nama warga, id pesanan, atau asetId
.toString() final matchesSearch =
.toLowerCase() sewa.wargaNama.toLowerCase().contains(query) ||
.contains(searchQuery.value.toLowerCase()); sewa.id.toLowerCase().contains(query) ||
(sewa.asetId != null &&
// Apply order ID filter if provided sewa.asetId!.toLowerCase().contains(query));
final matchesOrderId =
orderIdQuery.value.isEmpty ||
sewa['order_id'].toString().toLowerCase().contains(
orderIdQuery.value.toLowerCase(),
);
// Apply status filter if not 'Semua' // Apply status filter if not 'Semua'
final matchesStatus = final matchesStatus =
selectedStatusFilter.value == 'Semua' || selectedStatusFilter.value == 'Semua' ||
sewa['status'] == selectedStatusFilter.value; sewa.status.toUpperCase() ==
selectedStatusFilter.value.toUpperCase();
return matchesSearch && matchesOrderId && matchesStatus; return matchesSearch && matchesStatus;
}).toList(); }).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) // Load sewa data (mock data for now)
@ -68,100 +75,10 @@ class PetugasSewaController extends GetxController {
isLoading.value = true; isLoading.value = true;
try { try {
// Simulate API call delay final data = await SewaService().fetchAllSewa();
await Future.delayed(const Duration(milliseconds: 800)); // Sort data by tanggal_pemesanan in descending order (newest first)
data.sort((a, b) => b.tanggalPemesanan.compareTo(a.tanggalPemesanan));
// Populate with mock data sewaList.assignAll(data);
sewaList.assignAll([
{
'id': '1',
'order_id': 'SWA-001',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-02-05',
'tanggal_selesai': '2025-02-10',
'total_biaya': 45000,
'status': 'Diterima',
'photo_url': 'https://example.com/photo1.jpg',
},
{
'id': '2',
'order_id': 'SWA-002',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-02-15',
'tanggal_selesai': '2025-02-20',
'total_biaya': 30000,
'status': 'Selesai',
'photo_url': 'https://example.com/photo2.jpg',
},
{
'id': '3',
'order_id': 'SWA-003',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-02-25',
'tanggal_selesai': '2025-03-01',
'total_biaya': 35000,
'status': 'Menunggu Pembayaran',
'photo_url': 'https://example.com/photo3.jpg',
},
{
'id': '4',
'order_id': 'SWA-004',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-03-05',
'tanggal_selesai': '2025-03-08',
'total_biaya': 20000,
'status': 'Periksa Pembayaran',
'photo_url': 'https://example.com/photo4.jpg',
},
{
'id': '5',
'order_id': 'SWA-005',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-03-12',
'tanggal_selesai': '2025-03-14',
'total_biaya': 15000,
'status': 'Dibatalkan',
'photo_url': 'https://example.com/photo5.jpg',
},
{
'id': '6',
'order_id': 'SWA-006',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-03-18',
'tanggal_selesai': '2025-03-20',
'total_biaya': 25000,
'status': 'Pembayaran Denda',
'photo_url': 'https://example.com/photo6.jpg',
},
{
'id': '7',
'order_id': 'SWA-007',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-03-25',
'tanggal_selesai': '2025-03-28',
'total_biaya': 40000,
'status': 'Periksa Denda',
'photo_url': 'https://example.com/photo7.jpg',
},
{
'id': '8',
'order_id': 'SWA-008',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-04-02',
'tanggal_selesai': '2025-04-05',
'total_biaya': 10000,
'status': 'Dikembalikan',
'photo_url': 'https://example.com/photo8.jpg',
},
]);
} catch (e) { } catch (e) {
print('Error loading sewa data: $e'); print('Error loading sewa data: $e');
} finally { } finally {
@ -188,7 +105,9 @@ class PetugasSewaController extends GetxController {
void resetFilters() { void resetFilters() {
selectedStatusFilter.value = 'Semua'; selectedStatusFilter.value = 'Semua';
searchQuery.value = ''; 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() { void applyFilters() {
@ -196,14 +115,17 @@ class PetugasSewaController extends GetxController {
sewaList.where((sewa) { sewaList.where((sewa) {
bool matchesStatus = bool matchesStatus =
selectedStatusFilter.value == 'Semua' || selectedStatusFilter.value == 'Semua' ||
sewa['status'] == selectedStatusFilter.value; sewa.status.toUpperCase() ==
selectedStatusFilter.value.toUpperCase();
bool matchesSearch = bool matchesSearch =
searchQuery.value.isEmpty || searchQuery.value.isEmpty ||
sewa['nama_warga'].toLowerCase().contains( sewa.wargaNama.toLowerCase().contains(
searchQuery.value.toLowerCase(), searchQuery.value.toLowerCase(),
); );
return matchesStatus && matchesSearch; return matchesStatus && matchesSearch;
}).toList(); }).toList()
// Sort filtered results by tanggal_pemesanan in descending order (newest first)
..sort((a, b) => b.tanggalPemesanan.compareTo(a.tanggalPemesanan));
} }
// Format price to rupiah // Format price to rupiah
@ -213,102 +135,367 @@ class PetugasSewaController extends GetxController {
// Get color based on status // Get color based on status
Color getStatusColor(String status) { Color getStatusColor(String status) {
switch (status) { switch (status.toUpperCase()) {
case 'Menunggu Pembayaran': case 'MENUNGGU PEMBAYARAN':
return Colors.orange; return Colors.orangeAccent;
case 'Periksa Pembayaran': case 'PERIKSA PEMBAYARAN':
return Colors.amber.shade700; return Colors.amber;
case 'Diterima': case 'DITERIMA':
return Colors.blue; return Colors.blueAccent;
case 'Pembayaran Denda': case 'AKTIF':
return Colors.deepOrange;
case 'Periksa Denda':
return Colors.red.shade600;
case 'Dikembalikan':
return Colors.teal;
case 'Sedang Disewa':
return Colors.green; 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; return Colors.purple;
case 'Dibatalkan': case 'DIBATALKAN':
return Colors.red; return Colors.red;
default: default:
return Colors.grey; return Colors.grey;
} }
} }
// Handle sewa approval (from "Periksa Pembayaran" to "Diterima") // Get icon based on status
void approveSewa(String id) { IconData getStatusIcon(String status) {
final index = sewaList.indexWhere((sewa) => sewa['id'] == id); switch (status) {
if (index != -1) { case 'MENUNGGU PEMBAYARAN':
final sewa = Map<String, dynamic>.from(sewaList[index]); return Icons.payments_outlined;
final currentStatus = sewa['status']; case 'PERIKSA PEMBAYARAN':
return Icons.fact_check_outlined;
if (currentStatus == 'Periksa Pembayaran') { case 'DITERIMA':
sewa['status'] = 'Diterima'; return Icons.check_circle_outlined;
} else if (currentStatus == 'Periksa Denda') { case 'AKTIF':
sewa['status'] = 'Selesai'; return Icons.play_circle_outline;
} else if (currentStatus == 'Menunggu Pembayaran') { case 'PEMBYARAN DENDA':
sewa['status'] = 'Periksa Pembayaran'; 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;
}
} }
sewaList[index] = sewa; // Handle sewa approval (from "Periksa Pembayaran" to "Diterima")
void approveSewa(String id) {
final index = sewaList.indexWhere((sewa) => sewa.id == id);
if (index != -1) {
final sewa = 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.refresh();
} }
} }
}
// Handle sewa rejection or cancellation // Handle sewa rejection or cancellation
void rejectSewa(String id) { void rejectSewa(String id) {
final index = sewaList.indexWhere((sewa) => sewa['id'] == id); final index = sewaList.indexWhere((sewa) => sewa.id == id);
if (index != -1) { if (index != -1) {
final sewa = Map<String, dynamic>.from(sewaList[index]); final sewa = sewaList[index];
sewa['status'] = 'Dibatalkan'; sewaList[index] = SewaModel(
sewaList[index] = sewa; 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(); sewaList.refresh();
} }
} }
// Request payment for penalty // Request payment for penalty
void requestPenaltyPayment(String id) { void requestPenaltyPayment(String id) {
final index = sewaList.indexWhere((sewa) => sewa['id'] == id); final index = sewaList.indexWhere((sewa) => sewa.id == id);
if (index != -1) { if (index != -1) {
final sewa = Map<String, dynamic>.from(sewaList[index]); final sewa = sewaList[index];
sewa['status'] = 'Pembayaran Denda'; sewaList[index] = SewaModel(
sewaList[index] = sewa; 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(); sewaList.refresh();
} }
} }
// Mark penalty payment as requiring inspection // Mark penalty payment as requiring inspection
void markPenaltyForInspection(String id) { void markPenaltyForInspection(String id) {
final index = sewaList.indexWhere((sewa) => sewa['id'] == id); final index = sewaList.indexWhere((sewa) => sewa.id == id);
if (index != -1) { if (index != -1) {
final sewa = Map<String, dynamic>.from(sewaList[index]); final sewa = sewaList[index];
sewa['status'] = 'Periksa Denda'; sewaList[index] = SewaModel(
sewaList[index] = sewa; 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(); sewaList.refresh();
} }
} }
// Handle sewa completion // Handle sewa completion
void completeSewa(String id) { void completeSewa(String id) async {
final index = sewaList.indexWhere((sewa) => sewa['id'] == id); final index = sewaList.indexWhere((sewa) => sewa.id == id);
if (index != -1) { if (index != -1) {
final sewa = Map<String, dynamic>.from(sewaList[index]); final sewa = sewaList[index];
sewa['status'] = 'Selesai'; sewaList[index] = SewaModel(
sewaList[index] = sewa; 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(); sewaList.refresh();
// Update status in database
final asetProvider = Get.find<AsetProvider>();
await asetProvider.updateSewaAsetStatus(
sewaAsetId: id,
status: 'SELESAI',
);
} }
} }
// Mark rental as returned // Mark rental as returned
void markAsReturned(String id) { Future<void> markAsReturned(String id) async {
final index = sewaList.indexWhere((sewa) => sewa['id'] == id); final index = sewaList.indexWhere((sewa) => sewa.id == id);
if (index != -1) { if (index != -1) {
final sewa = Map<String, dynamic>.from(sewaList[index]); final sewa = sewaList[index];
sewa['status'] = 'Dikembalikan'; sewaList[index] = SewaModel(
sewaList[index] = sewa; 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(); 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,
);
} }
} }
} }

View File

@ -1,7 +1,200 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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 { 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 // Form controllers
final nameController = TextEditingController(); final nameController = TextEditingController();
final descriptionController = TextEditingController(); final descriptionController = TextEditingController();
@ -23,28 +216,16 @@ class PetugasTambahAsetController extends GetxController {
final categoryOptions = ['Sewa', 'Langganan']; final categoryOptions = ['Sewa', 'Langganan'];
final statusOptions = ['Tersedia', 'Pemeliharaan']; final statusOptions = ['Tersedia', 'Pemeliharaan'];
// Images // List to store selected images
final selectedImages = <String>[].obs; final RxList<XFile> selectedImages = <XFile>[].obs;
// List to store network image URLs
final RxList<String> networkImageUrls = <String>[].obs;
final _picker = ImagePicker();
// Form validation // Form validation
final isFormValid = false.obs; final isFormValid = false.obs;
final isSubmitting = 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 @override
void onClose() { void onClose() {
// Dispose controllers // Dispose controllers
@ -89,17 +270,142 @@ class PetugasTambahAsetController extends GetxController {
validateForm(); validateForm();
} }
// Add image to the list (in a real app, this would handle file upload) // Create a new asset in Supabase
void addImage(String imagePath) { Future<String?> _createAsset(
selectedImages.add(imagePath); Map<String, dynamic> assetData,
validateForm(); 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;
} }
// Remove image from the list 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;
}
}
// 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) { void removeImage(int index) {
if (index >= 0 && index < selectedImages.length) { 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); selectedImages.removeAt(index);
validateForm();
} }
} }
@ -133,62 +439,132 @@ class PetugasTambahAsetController extends GetxController {
basicValid && perHourValid && perDayValid && anyTimeOptionSelected; basicValid && perHourValid && perDayValid && anyTimeOptionSelected;
} }
// Submit form and save asset // Submit form and save or update asset
Future<void> saveAsset() async { Future<void> saveAsset() async {
if (!isFormValid.value) return; if (!isFormValid.value) return;
isSubmitting.value = true; isSubmitting.value = true;
try { try {
// In a real app, this would make an API call to save the asset // Prepare the basic asset data
await Future.delayed(const Duration(seconds: 1)); // Mock API call final Map<String, dynamic> assetData = {
// Prepare asset data
final assetData = {
'nama': nameController.text, 'nama': nameController.text,
'deskripsi': descriptionController.text, 'deskripsi': descriptionController.text,
'kategori': selectedCategory.value, 'kategori': 'sewa', // Default to 'sewa' category
'status': selectedStatus.value, 'status': selectedStatus.value,
'kuantitas': int.parse(quantityController.text), 'kuantitas': int.parse(quantityController.text),
'satuan_ukur': unitOfMeasureController.text, 'satuan_ukur': 'unit', // Default unit of measure
'opsi_waktu_sewa':
timeOptions.entries
.where((entry) => entry.value.value)
.map((entry) => entry.key)
.toList(),
'harga_per_jam':
timeOptions['Per Jam']!.value
? int.parse(pricePerHourController.text)
: null,
'max_jam':
timeOptions['Per Jam']!.value && maxHourController.text.isNotEmpty
? int.parse(maxHourController.text)
: null,
'harga_per_hari':
timeOptions['Per Hari']!.value
? int.parse(pricePerDayController.text)
: null,
'max_hari':
timeOptions['Per Hari']!.value && maxDayController.text.isNotEmpty
? int.parse(maxDayController.text)
: null,
'gambar': selectedImages,
}; };
// Log the data (in a real app, this would be sent to an API) // Handle time options and pricing
print('Asset data: $assetData'); final List<Map<String, dynamic>> satuanWaktuSewa = [];
// Return to the asset list page if (timeOptions['Per Jam']?.value == true) {
Get.back(); final hargaPerJam = int.tryParse(pricePerHourController.text) ?? 0;
final maxJam = int.tryParse(maxHourController.text) ?? 24;
if (hargaPerJam <= 0) {
throw Exception('Harga per jam harus lebih dari 0');
}
satuanWaktuSewa.add({
'satuan_waktu': 'jam',
'harga': hargaPerJam,
'maksimal_waktu': maxJam,
});
}
if (timeOptions['Per Hari']?.value == true) {
final hargaPerHari = int.tryParse(pricePerDayController.text) ?? 0;
final maxHari = int.tryParse(maxDayController.text) ?? 30;
if (hargaPerHari <= 0) {
throw Exception('Harga per hari harus lebih dari 0');
}
satuanWaktuSewa.add({
'satuan_waktu': 'hari',
'harga': hargaPerHari,
'maksimal_waktu': maxHari,
});
}
// 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 // Show success message
Get.snackbar( Get.snackbar(
'Berhasil', 'Sukses',
'Aset berhasil ditambahkan', isEditing.value
? 'Aset berhasil diperbarui'
: 'Aset berhasil ditambahkan',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, 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) { } catch (e) {
// Show error message // Show error message
Get.snackbar( Get.snackbar(
@ -196,15 +572,76 @@ class PetugasTambahAsetController extends GetxController {
'Terjadi kesalahan: ${e.toString()}', 'Terjadi kesalahan: ${e.toString()}',
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} finally { } finally {
isSubmitting.value = false; 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 // For demonstration purposes: add sample image
void addSampleImage() { 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();
} }
} }

View File

@ -1,5 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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 { class PetugasTambahPaketController extends GetxController {
// Form controllers // Form controllers
@ -10,14 +17,14 @@ class PetugasTambahPaketController extends GetxController {
// Dropdown and toggle values // Dropdown and toggle values
final selectedCategory = 'Bulanan'.obs; final selectedCategory = 'Bulanan'.obs;
final selectedStatus = 'Aktif'.obs; final selectedStatus = 'Tersedia'.obs;
// Category options // Category options
final categoryOptions = ['Bulanan', 'Tahunan', 'Premium', 'Bisnis']; final categoryOptions = ['Bulanan', 'Tahunan', 'Premium', 'Bisnis'];
final statusOptions = ['Aktif', 'Nonaktif']; final statusOptions = ['Tersedia', 'Pemeliharaan'];
// Images // Images
final selectedImages = <String>[].obs; final selectedImages = <dynamic>[].obs;
// For package name and description // For package name and description
final packageNameController = TextEditingController(); final packageNameController = TextEditingController();
@ -31,21 +38,90 @@ class PetugasTambahPaketController extends GetxController {
// For asset selection // For asset selection
final RxList<Map<String, dynamic>> availableAssets = final RxList<Map<String, dynamic>> availableAssets =
<Map<String, dynamic>>[].obs; <Map<String, dynamic>>[].obs;
final Rx<int?> selectedAsset = Rx<int?>(null); final Rx<String?> selectedAsset = Rx<String?>(null);
final RxBool isLoadingAssets = false.obs; final RxBool isLoadingAssets = false.obs;
// Form validation // Form validation
final isFormValid = false.obs; final isFormValid = false.obs;
final isSubmitting = 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 @override
void onInit() { void onInit() {
super.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 // Listen to field changes for validation
nameController.addListener(validateForm); nameController.addListener(() {
descriptionController.addListener(validateForm); validateForm();
priceController.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 // Load available assets when the controller initializes
fetchAvailableAssets(); fetchAvailableAssets();
@ -61,6 +137,10 @@ class PetugasTambahPaketController extends GetxController {
packageNameController.dispose(); packageNameController.dispose();
packageDescriptionController.dispose(); packageDescriptionController.dispose();
packagePriceController.dispose(); packagePriceController.dispose();
pricePerHourController.dispose();
maxHourController.dispose();
pricePerDayController.dispose();
maxDayController.dispose();
super.onClose(); super.onClose();
} }
@ -68,18 +148,21 @@ class PetugasTambahPaketController extends GetxController {
void setCategory(String category) { void setCategory(String category) {
selectedCategory.value = category; selectedCategory.value = category;
validateForm(); validateForm();
checkFormChanged();
} }
// Change selected status // Change selected status
void setStatus(String status) { void setStatus(String status) {
selectedStatus.value = status; selectedStatus.value = status;
validateForm(); validateForm();
checkFormChanged();
} }
// Add image to the list (in a real app, this would handle file upload) // Add image to the list (in a real app, this would handle file upload)
void addImage(String imagePath) { void addImage(String imagePath) {
selectedImages.add(imagePath); selectedImages.add(imagePath);
validateForm(); validateForm();
checkFormChanged();
} }
// Remove image from the list // Remove image from the list
@ -87,34 +170,43 @@ class PetugasTambahPaketController extends GetxController {
if (index >= 0 && index < selectedImages.length) { if (index >= 0 && index < selectedImages.length) {
selectedImages.removeAt(index); selectedImages.removeAt(index);
validateForm(); validateForm();
checkFormChanged();
} }
} }
// Fetch available assets from the API or local data // Fetch available assets from Supabase and filter out already selected ones
void fetchAvailableAssets() { Future<void> fetchAvailableAssets() async {
isLoadingAssets.value = true; isLoadingAssets.value = true;
try {
// This is a mock implementation - replace with actual API call final allAssets = await _asetProvider.getSewaAsets();
Future.delayed(const Duration(seconds: 1), () { final selectedAsetIds =
availableAssets.value = [ packageItems.map((item) => item['asetId'].toString()).toSet();
{'id': 1, 'nama': 'Laptop Dell XPS', 'stok': 5}, // Only show assets not yet selected
{'id': 2, 'nama': 'Proyektor Epson', 'stok': 3}, availableAssets.value =
{'id': 3, 'nama': 'Meja Kantor', 'stok': 10}, allAssets
{'id': 4, 'nama': 'Kursi Ergonomis', 'stok': 15}, .where((aset) => !selectedAsetIds.contains(aset.id))
{'id': 5, 'nama': 'Printer HP LaserJet', 'stok': 2}, .map(
{'id': 6, 'nama': 'AC Panasonic 1PK', 'stok': 8}, (aset) => {
]; 'id': aset.id,
'nama': aset.nama,
'stok': aset.kuantitas,
},
)
.toList();
} catch (e) {
availableAssets.value = [];
} finally {
isLoadingAssets.value = false; isLoadingAssets.value = false;
}); }
} }
// Set the selected asset // Set the selected asset
void setSelectedAsset(int? assetId) { void setSelectedAsset(String? assetId) {
selectedAsset.value = assetId; selectedAsset.value = assetId;
} }
// Get remaining stock for an asset (considering current selections) // Get remaining stock for an asset (considering current selections)
int getRemainingStock(int assetId) { int getRemainingStock(String assetId) {
// Find the asset in available assets // Find the asset in available assets
final asset = availableAssets.firstWhere( final asset = availableAssets.firstWhere(
(item) => item['id'] == assetId, (item) => item['id'] == assetId,
@ -129,7 +221,7 @@ class PetugasTambahPaketController extends GetxController {
// Calculate how many of this asset are already in the package // Calculate how many of this asset are already in the package
int alreadySelected = 0; int alreadySelected = 0;
for (var item in packageItems) { for (var item in packageItems) {
if (item['asetId'] == assetId) { if (item['asetId'].toString() == assetId) {
alreadySelected += item['jumlah'] as int; alreadySelected += item['jumlah'] as int;
} }
} }
@ -144,7 +236,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Pilih aset dan masukkan jumlah', 'Pilih aset dan masukkan jumlah',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -165,7 +257,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Jumlah harus lebih dari 0', 'Jumlah harus lebih dari 0',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -178,7 +270,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Jumlah melebihi stok yang tersedia', 'Jumlah melebihi stok yang tersedia',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -200,10 +292,12 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Item berhasil ditambahkan ke paket', 'Item berhasil ditambahkan ke paket',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
checkFormChanged();
} }
// Update an existing package item // Update an existing package item
@ -212,7 +306,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Pilih aset dan masukkan jumlah', 'Pilih aset dan masukkan jumlah',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -233,7 +327,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Jumlah harus lebih dari 0', 'Jumlah harus lebih dari 0',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -261,7 +355,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Jumlah melebihi stok yang tersedia', 'Jumlah melebihi stok yang tersedia',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -274,7 +368,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Jumlah melebihi stok yang tersedia', 'Jumlah melebihi stok yang tersedia',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -297,19 +391,24 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Item berhasil diperbarui', 'Item berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
checkFormChanged();
} }
// Remove an item from the package // Remove an item from the package
void removeItem(int index) { void removeItem(int index) {
if (index >= 0 && index < packageItems.length) {
packageItems.removeAt(index); packageItems.removeAt(index);
checkFormChanged();
}
Get.snackbar( Get.snackbar(
'Dihapus', 'Dihapus',
'Item berhasil dihapus dari paket', 'Item berhasil dihapus dari paket',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
colorText: Colors.white, colorText: Colors.white,
); );
@ -319,10 +418,7 @@ class PetugasTambahPaketController extends GetxController {
void validateForm() { void validateForm() {
// Basic validation // Basic validation
bool basicValid = bool basicValid =
nameController.text.isNotEmpty && nameController.text.isNotEmpty && descriptionController.text.isNotEmpty;
descriptionController.text.isNotEmpty &&
priceController.text.isNotEmpty &&
int.tryParse(priceController.text) != null;
// Package should have at least one item // Package should have at least one item
bool hasItems = packageItems.isNotEmpty; bool hasItems = packageItems.isNotEmpty;
@ -337,42 +433,207 @@ class PetugasTambahPaketController extends GetxController {
isSubmitting.value = true; isSubmitting.value = true;
try { try {
// In a real app, this would make an API call to save the package final supabase = Supabase.instance.client;
await Future.delayed(const Duration(seconds: 1)); // Mock API call 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 // 1. Update data utama paket
final paketData = { await supabase
.from('paket')
.update({
'nama': nameController.text, 'nama': nameController.text,
'deskripsi': descriptionController.text, 'deskripsi': descriptionController.text,
'kategori': selectedCategory.value, 'status': selectedStatus.value.toLowerCase(),
'status': selectedStatus.value == 'Aktif', })
'harga': int.parse(priceController.text), .eq('id', paketId);
'gambar': selectedImages,
'items': packageItems,
};
// Log the data (in a real app, this would be sent to an API) // 2. Update paket_item: hapus semua, insert ulang
print('Package data: $paketData'); 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 // 3. Update satuan_waktu_sewa: hapus semua, insert ulang
Get.back(); 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 // 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( Get.snackbar(
'Berhasil', 'Berhasil',
'Paket berhasil ditambahkan', 'Paket berhasil ditambahkan',
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}
} catch (e) { } catch (e) {
// Show error message // Show error message
Get.snackbar( Get.snackbar(
'Gagal', 'Gagal',
'Terjadi kesalahan: ${e.toString()}', 'Terjadi kesalahan: \\${e.toString()}',
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} finally { } finally {
isSubmitting.value = false; isSubmitting.value = false;
@ -390,4 +651,215 @@ class PetugasTambahPaketController extends GetxController {
selectedImages.add('https://example.com/sample_image.jpg'); selectedImages.add('https://example.com/sample_image.jpg');
validateForm(); 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();
}
} }

View File

@ -476,7 +476,7 @@ class ListPetugasMitraView extends GetView<ListPetugasMitraController> {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Harap isi semua data', 'Harap isi semua data',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -600,7 +600,7 @@ class ListPetugasMitraView extends GetView<ListPetugasMitraController> {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Harap isi semua data', 'Harap isi semua data',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );

View File

@ -203,7 +203,7 @@ class ListTagihanPeriodeView extends GetView<ListTagihanPeriodeController> {
backgroundColor: Colors.orange.withOpacity(0.1), backgroundColor: Colors.orange.withOpacity(0.1),
colorText: Colors.orange.shade800, colorText: Colors.orange.shade800,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
margin: const EdgeInsets.all(8), margin: const EdgeInsets.all(8),
); );
}, },

View 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'),
),
],
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/petugas_aset_controller.dart'; import '../controllers/petugas_aset_controller.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../../../theme/app_colors_petugas.dart'; import '../../../theme/app_colors_petugas.dart';
import '../widgets/petugas_bumdes_bottom_navbar.dart'; import '../widgets/petugas_bumdes_bottom_navbar.dart';
import '../widgets/petugas_side_navbar.dart'; import '../widgets/petugas_side_navbar.dart';
@ -23,26 +24,12 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
void initState() { void initState() {
super.initState(); super.initState();
controller = Get.find<PetugasAsetController>(); controller = Get.find<PetugasAsetController>();
_tabController = TabController(length: 2, vsync: this); // Initialize with default tab (sewa)
controller.changeTab(0);
// 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);
}
});
} }
@override @override
void dispose() { void dispose() {
_tabController.dispose();
super.dispose(); super.dispose();
} }
@ -82,7 +69,7 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
body: Column( body: Column(
children: [ children: [
_buildSearchBar(), _buildSearchBar(),
_buildTabBar(), const SizedBox(height: 16),
Expanded(child: _buildAssetList()), Expanded(child: _buildAssetList()),
], ],
), ),
@ -93,7 +80,17 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
), ),
), ),
floatingActionButton: FloatingActionButton.extended( 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, backgroundColor: AppColorsPetugas.babyBlueBright,
icon: Icon(Icons.add, color: AppColorsPetugas.blueGrotto), icon: Icon(Icons.add, color: AppColorsPetugas.blueGrotto),
label: Text( label: Text(
@ -144,60 +141,19 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
); );
} }
Widget _buildTabBar() { // Tab bar has been removed as per requirements
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),
),
],
),
),
),
],
),
);
}
Widget _buildAssetList() { Widget _buildAssetList() {
return Obx(() { 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) { if (controller.isLoading.value) {
return Center( return Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(
@ -255,10 +211,15 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
color: AppColorsPetugas.blueGrotto, color: AppColorsPetugas.blueGrotto,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
itemCount: controller.filteredAsetList.length, itemCount: controller.filteredAsetList.length + 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index < controller.filteredAsetList.length) {
final aset = controller.filteredAsetList[index]; final aset = controller.filteredAsetList[index];
return _buildAssetCard(context, aset); 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) { 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( return Container(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
@ -286,28 +271,52 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: () => _showAssetDetails(context, aset),
child: Row( child: Row(
children: [ children: [
// Asset image // Asset image
Container( SizedBox(
width: 80, width: 80,
height: 80, height: 80,
decoration: BoxDecoration( child: ClipRRect(
color: AppColorsPetugas.babyBlueLight,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12), topLeft: Radius.circular(12),
bottomLeft: Radius.circular(12), bottomLeft: Radius.circular(12),
), ),
), child: CachedNetworkImage(
imageUrl: imageUrl,
fit: BoxFit.cover,
placeholder:
(context, url) => Container(
color: AppColorsPetugas.babyBlueLight,
child: Center( child: Center(
child: Icon( child: Icon(
_getAssetIcon(aset['kategori']), _getAssetIcon(
color: AppColorsPetugas.navyBlue, kategori,
), // Show category icon as placeholder
color: AppColorsPetugas.navyBlue.withOpacity(
0.5,
),
size: 32, 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,
),
),
),
),
),
),
// Asset info // Asset info
Expanded( Expanded(
@ -323,8 +332,8 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
aset['nama'], nama,
style: TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
color: AppColorsPetugas.navyBlue, color: AppColorsPetugas.navyBlue,
@ -333,12 +342,63 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( // Harga dan satuan waktu (multi-line, tampilkan semua dari satuanWaktuSewa)
'${controller.formatPrice(aset['harga'])} ${aset['satuan']}', 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( style: TextStyle(
fontSize: 12, fontSize: 12,
color: AppColorsPetugas.textSecondary, color: AppColorsPetugas.textSecondary,
), ),
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
}
},
), ),
], ],
), ),
@ -383,11 +443,42 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
children: [ children: [
// Edit icon // Edit icon
GestureDetector( GestureDetector(
onTap: onTap: () {
() => _showAddEditAssetDialog( // Navigate to PetugasTambahAsetView in edit mode with only the asset ID
context, final assetId =
aset: aset, 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( child: Container(
padding: const EdgeInsets.all(5), padding: const EdgeInsets.all(5),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -589,590 +680,16 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
} }
} }
void _showAssetDetails(BuildContext context, Map<String, dynamic> aset) { void _showAddEditAssetDialog(BuildContext context) {
final isAvailable = aset['tersedia'] == true; // Navigate to PetugasTambahAsetView in add mode and refresh data when returning
Get.toNamed(
showModalBottomSheet( Routes.PETUGAS_TAMBAH_ASET,
context: context, arguments: {'isEditing': false, 'assetData': null},
isScrollControlled: true, )?.then((_) {
backgroundColor: Colors.transparent, // Refresh data when returning from tambah_aset page
builder: (context) { debugPrint('Returning from tambah aset page, refreshing data...');
return Container( controller.loadAsetData();
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 _showDeleteConfirmation( void _showDeleteConfirmation(
@ -1251,22 +768,11 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () async {
Navigator.pop(context); Navigator.pop(context);
controller.deleteAset(aset['id']); // Let the controller handle the deletion and showing the snackbar
Get.snackbar( await controller.deleteAset(aset['id']);
'Aset Dihapus', // The controller will show appropriate success or error messages
'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,
),
);
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColorsPetugas.error, backgroundColor: AppColorsPetugas.error,

View File

@ -327,7 +327,7 @@ class PetugasBumdesCbpView extends GetView<PetugasBumdesCbpController> {
leading: const Icon(Icons.subscriptions_outlined), leading: const Icon(Icons.subscriptions_outlined),
title: const Text('Kelola Langganan'), title: const Text('Kelola Langganan'),
onTap: () { onTap: () {
Get.offAllNamed(Routes.PETUGAS_LANGGANAN); Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
}, },
), ),
ListTile( ListTile(

View File

@ -5,6 +5,8 @@ import '../controllers/petugas_bumdes_dashboard_controller.dart';
import '../widgets/petugas_bumdes_bottom_navbar.dart'; import '../widgets/petugas_bumdes_bottom_navbar.dart';
import '../widgets/petugas_side_navbar.dart'; import '../widgets/petugas_side_navbar.dart';
import '../../../theme/app_colors_petugas.dart'; import '../../../theme/app_colors_petugas.dart';
import '../../../utils/format_utils.dart';
import '../views/petugas_penyewa_view.dart';
class PetugasBumdesDashboardView class PetugasBumdesDashboardView
extends GetView<PetugasBumdesDashboardController> { extends GetView<PetugasBumdesDashboardController> {
@ -23,12 +25,7 @@ class PetugasBumdesDashboardView
backgroundColor: AppColorsPetugas.navyBlue, backgroundColor: AppColorsPetugas.navyBlue,
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 0, elevation: 0,
actions: [ // actions: [],
IconButton(
icon: const Icon(Icons.logout),
onPressed: () => _showLogoutConfirmation(context),
),
],
), ),
drawer: PetugasSideNavbar(controller: controller), drawer: PetugasSideNavbar(controller: controller),
drawerEdgeDragWidth: 60, drawerEdgeDragWidth: 60,
@ -68,6 +65,8 @@ class PetugasBumdesDashboardView
case 3: case 3:
return 'Permintaan Sewa'; return 'Permintaan Sewa';
case 4: case 4:
return 'Penyewa';
case 5:
return 'Profil BUMDes'; return 'Profil BUMDes';
default: default:
return 'Dashboard Petugas BUMDES'; return 'Dashboard Petugas BUMDES';
@ -85,6 +84,8 @@ class PetugasBumdesDashboardView
case 3: case 3:
return _buildSewaTab(); return _buildSewaTab();
case 4: case 4:
return const PetugasPenyewaView();
case 5:
return _buildBumdesTab(); return _buildBumdesTab();
default: default:
return _buildDashboardTab(); return _buildDashboardTab();
@ -100,6 +101,16 @@ class PetugasBumdesDashboardView
_buildWelcomeCard(), _buildWelcomeCard(),
const SizedBox(height: 24), 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 // Detail Status Sewa Aset section with improved header
_buildSectionHeader( _buildSectionHeader(
'Detail Status Sewa Aset', 'Detail Status Sewa Aset',
@ -118,8 +129,6 @@ class PetugasBumdesDashboardView
), ),
_buildRevenueStatistics(), _buildRevenueStatistics(),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildRevenueSources(),
const SizedBox(height: 16),
_buildRevenueTrend(), _buildRevenueTrend(),
// Add some padding at the bottom for better scrolling // Add some padding at the bottom for better scrolling
@ -156,7 +165,31 @@ class PetugasBumdesDashboardView
children: [ children: [
Row( Row(
children: [ children: [
Container( 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,
),
),
),
);
} else {
return Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2), color: Colors.white.withOpacity(0.2),
@ -174,7 +207,9 @@ class PetugasBumdesDashboardView
color: Colors.white, color: Colors.white,
size: 30, size: 30,
), ),
), );
}
}),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Column( child: Column(
@ -208,15 +243,17 @@ class PetugasBumdesDashboardView
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Obx( Obx(() {
() => Text( final name = controller.userName.value;
controller.userEmail.value, final email = controller.userEmail.value;
return Text(
name.isNotEmpty ? name : email,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.white70, color: Colors.white70,
), ),
), );
), }),
], ],
), ),
), ),
@ -642,19 +679,24 @@ class PetugasBumdesDashboardView
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Obx( Obx(() {
() => Text( final stats = controller.pembayaranStats;
controller.totalPendapatanBulanIni.value, final total = stats['totalThisMonth'] ?? 0.0;
return Text(
formatRupiah(total),
style: TextStyle( style: TextStyle(
fontSize: 28, fontSize: 28,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColorsPetugas.success, color: AppColorsPetugas.success,
), ),
), );
), }),
const SizedBox(height: 6), const SizedBox(height: 6),
Obx( Obx(() {
() => Row( final stats = controller.pembayaranStats;
final percent = stats['percentComparedLast'] ?? 0.0;
final isPositive = percent >= 0;
return Row(
children: [ children: [
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -663,7 +705,7 @@ class PetugasBumdesDashboardView
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color:
controller.isKenaikanPositif.value isPositive
? AppColorsPetugas.success.withOpacity( ? AppColorsPetugas.success.withOpacity(
0.1, 0.1,
) )
@ -676,23 +718,23 @@ class PetugasBumdesDashboardView
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon( Icon(
controller.isKenaikanPositif.value isPositive
? Icons.arrow_upward ? Icons.arrow_upward
: Icons.arrow_downward, : Icons.arrow_downward,
size: 14, size: 14,
color: color:
controller.isKenaikanPositif.value isPositive
? AppColorsPetugas.success ? AppColorsPetugas.success
: AppColorsPetugas.error, : AppColorsPetugas.error,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
controller.persentaseKenaikan.value, '${percent.toStringAsFixed(1)}%',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: color:
controller.isKenaikanPositif.value isPositive
? AppColorsPetugas.success ? AppColorsPetugas.success
: AppColorsPetugas.error, : AppColorsPetugas.error,
), ),
@ -709,8 +751,8 @@ class PetugasBumdesDashboardView
), ),
), ),
], ],
), );
), }),
], ],
), ),
), ),
@ -744,16 +786,29 @@ class PetugasBumdesDashboardView
} }
Widget _buildRevenueSummary() { Widget _buildRevenueSummary() {
return Row( return Column(
children: [ children: [
Expanded( Obx(() {
child: _buildRevenueQuickInfo( final stats = controller.pembayaranStats;
'Pendapatan Sewa', final totalTunai = stats['totalTunai'] ?? 0.0;
controller.pendapatanSewa.value, return _buildRevenueQuickInfo(
'Tunai',
formatRupiah(totalTunai),
AppColorsPetugas.navyBlue, 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() { Widget _buildRevenueTrend() {
final months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun']; final months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun'];
@ -912,6 +892,9 @@ class PetugasBumdesDashboardView
child: Obx(() { child: Obx(() {
// Get the trend data from controller // Get the trend data from controller
final List<double> trendData = controller.trendPendapatan; final List<double> trendData = controller.trendPendapatan;
if (trendData.isEmpty) {
return Center(child: Text('Tidak ada data'));
}
final double maxValue = trendData.reduce( final double maxValue = trendData.reduce(
(curr, next) => curr > next ? curr : next, (curr, next) => curr > next ? curr : next,
); );
@ -925,28 +908,28 @@ class PetugasBumdesDashboardView
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text( Text(
'${maxValue.toStringAsFixed(1)}M', formatRupiah(maxValue),
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: AppColorsPetugas.textSecondary, color: AppColorsPetugas.textSecondary,
), ),
), ),
Text( Text(
'${(maxValue * 0.75).toStringAsFixed(1)}M', formatRupiah(maxValue * 0.75),
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: AppColorsPetugas.textSecondary, color: AppColorsPetugas.textSecondary,
), ),
), ),
Text( Text(
'${(maxValue * 0.5).toStringAsFixed(1)}M', formatRupiah(maxValue * 0.5),
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: AppColorsPetugas.textSecondary, color: AppColorsPetugas.textSecondary,
), ),
), ),
Text( Text(
'${(maxValue * 0.25).toStringAsFixed(1)}M', formatRupiah(maxValue * 0.25),
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: AppColorsPetugas.textSecondary, color: AppColorsPetugas.textSecondary,
@ -1004,7 +987,13 @@ class PetugasBumdesDashboardView
children: [ children: [
Container( Container(
width: 35, width: 35,
height: 170 * percentage, height:
percentage.isNaN || percentage <= 0
? 10.0
: (170 * percentage).clamp(
10.0,
170.0,
),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: borderRadius:
const BorderRadius.vertical( 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 // Custom clipper for creating pie/donut chart segments

File diff suppressed because it is too large Load Diff

View 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,
),
),
),
],
),
],
);
}),
),
],
),
),
);
}
}

View File

@ -567,7 +567,7 @@ class PetugasManajemenBumdesView
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Fitur tambah rekening bank sedang dalam pengembangan', 'Fitur tambah rekening bank sedang dalam pengembangan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -639,7 +639,7 @@ class PetugasManajemenBumdesView
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Fitur edit rekening bank sedang dalam pengembangan', 'Fitur edit rekening bank sedang dalam pengembangan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -689,7 +689,7 @@ class PetugasManajemenBumdesView
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Fitur hapus rekening bank sedang dalam pengembangan', 'Fitur hapus rekening bank sedang dalam pengembangan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -765,7 +765,7 @@ class PetugasManajemenBumdesView
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Fitur tambah mitra sedang dalam pengembangan', 'Fitur tambah mitra sedang dalam pengembangan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -845,7 +845,7 @@ class PetugasManajemenBumdesView
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Fitur edit mitra sedang dalam pengembangan', 'Fitur edit mitra sedang dalam pengembangan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -895,7 +895,7 @@ class PetugasManajemenBumdesView
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Fitur hapus mitra sedang dalam pengembangan', 'Fitur hapus mitra sedang dalam pengembangan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(

View File

@ -1,11 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/petugas_paket_controller.dart'; import 'package:bumrent_app/app/modules/petugas_bumdes/controllers/petugas_paket_controller.dart';
import '../../../theme/app_colors_petugas.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_bumdes_bottom_navbar.dart';
import '../widgets/petugas_side_navbar.dart'; import '../widgets/petugas_side_navbar.dart';
import '../controllers/petugas_bumdes_dashboard_controller.dart'; import '../controllers/petugas_bumdes_dashboard_controller.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../../../theme/app_colors_petugas.dart';
class PetugasPaketView extends GetView<PetugasPaketController> { class PetugasPaketView extends GetView<PetugasPaketController> {
const PetugasPaketView({Key? key}) : super(key: key); const PetugasPaketView({Key? key}) : super(key: key);
@ -53,7 +55,17 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
), ),
), ),
floatingActionButton: FloatingActionButton.extended( 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( label: Text(
'Tambah Paket', 'Tambah Paket',
style: TextStyle( style: TextStyle(
@ -115,7 +127,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
); );
} }
if (controller.filteredPaketList.isEmpty) { if (controller.filteredPackages.isEmpty) {
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -136,7 +148,17 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( 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), icon: const Icon(Icons.add),
label: const Text('Tambah Paket'), label: const Text('Tambah Paket'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -161,18 +183,192 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
color: AppColorsPetugas.blueGrotto, color: AppColorsPetugas.blueGrotto,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
itemCount: controller.filteredPaketList.length, itemCount: controller.filteredPackages.length + 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final paket = controller.filteredPaketList[index]; if (index < controller.filteredPackages.length) {
final paket = controller.filteredPackages[index];
return _buildPaketCard(context, paket); return _buildPaketCard(context, paket);
} else {
// Blank space at the end
return const SizedBox(height: 80);
}
}, },
), ),
); );
}); });
} }
Widget _buildPaketCard(BuildContext context, Map<String, dynamic> paket) { // Format price helper method
final isAvailable = paket['tersedia'] == true; 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( return Container(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
@ -192,28 +388,103 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( 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( child: Row(
children: [ children: [
// Paket image or icon // Paket image or icon
Container( SizedBox(
width: 80, width: 80,
height: 80, height: 80,
decoration: BoxDecoration( child: ClipRRect(
color: AppColorsPetugas.babyBlueLight,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12), topLeft: Radius.circular(12),
bottomLeft: Radius.circular(12), bottomLeft: Radius.circular(12),
), ),
), 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: Center(
child: Icon( child: Icon(
_getPaketIcon(paket['kategori']), _getPaketIcon(
color: AppColorsPetugas.navyBlue, _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, 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,
),
),
),
),
),
// Paket info // Paket info
Expanded( Expanded(
@ -228,9 +499,10 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Package name
Text( Text(
paket['nama'], nama,
style: TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
color: AppColorsPetugas.navyBlue, color: AppColorsPetugas.navyBlue,
@ -239,6 +511,111 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
// 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( Text(
'Rp ${_formatPrice(paket['harga'])}', 'Rp ${_formatPrice(paket['harga'])}',
style: TextStyle( style: TextStyle(
@ -247,6 +624,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
), ),
), ),
], ],
],
), ),
), ),
@ -258,25 +636,31 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color:
isAvailable status.toLowerCase() == 'tersedia'
? AppColorsPetugas.successLight ? AppColorsPetugas.successLight
: status.toLowerCase() == 'pemeliharaan'
? AppColorsPetugas.warningLight
: AppColorsPetugas.errorLight, : AppColorsPetugas.errorLight,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all( border: Border.all(
color: color:
isAvailable status.toLowerCase() == 'tersedia'
? AppColorsPetugas.success ? AppColorsPetugas.success
: status.toLowerCase() == 'pemeliharaan'
? AppColorsPetugas.warning
: AppColorsPetugas.error, : AppColorsPetugas.error,
width: 1, width: 1,
), ),
), ),
child: Text( child: Text(
isAvailable ? 'Aktif' : 'Nonaktif', status,
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: color:
isAvailable status.toLowerCase() == 'tersedia'
? AppColorsPetugas.success ? AppColorsPetugas.success
: status.toLowerCase() == 'pemeliharaan'
? AppColorsPetugas.warning
: AppColorsPetugas.error, : AppColorsPetugas.error,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
@ -290,9 +674,12 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
// Edit icon // Edit icon
GestureDetector( GestureDetector(
onTap: onTap:
() => _showAddEditPaketDialog( () => Get.toNamed(
context, Routes.PETUGAS_TAMBAH_PAKET,
paket: paket, arguments: {
'isEditing': true,
'paket': paket,
},
), ),
child: Container( child: Container(
padding: const EdgeInsets.all(5), padding: const EdgeInsets.all(5),
@ -350,33 +737,42 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
); );
} }
String _formatPrice(dynamic price) { // Add this helper method to get color based on status
if (price == null) return '0'; Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
// Convert the price to string and handle formatting case 'aktif':
String priceStr = price.toString(); return AppColorsPetugas.success;
case 'tidak aktif':
// Add thousand separators case 'nonaktif':
final RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'); return AppColorsPetugas.error;
String formatted = priceStr.replaceAllMapped(reg, (Match m) => '${m[1]}.'); case 'dalam perbaikan':
case 'maintenance':
return formatted; return AppColorsPetugas.warning;
case 'tersedia':
return AppColorsPetugas.success;
case 'pemeliharaan':
return AppColorsPetugas.warning;
default:
return Colors.grey;
}
} }
IconData _getPaketIcon(String? category) { IconData _getPaketIcon(String? timeUnit) {
if (category == null) return Icons.category; if (timeUnit == null) return Icons.access_time;
switch (category.toLowerCase()) { switch (timeUnit.toLowerCase()) {
case 'bulanan': case 'jam':
return Icons.calendar_month; return Icons.access_time;
case 'tahunan': case 'hari':
return Icons.calendar_today; return Icons.calendar_today;
case 'premium': case 'minggu':
return Icons.star; return Icons.date_range;
case 'bisnis': case 'bulan':
return Icons.business; return Icons.calendar_month;
case 'tahun':
return Icons.calendar_view_month;
default: 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) { void _showDeleteConfirmation(BuildContext context, dynamic paket) {
showModalBottomSheet( // 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, context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) { builder: (context) {
return Container( return Dialog(
padding: const EdgeInsets.all(16), shape: RoundedRectangleBorder(
constraints: BoxConstraints( borderRadius: BorderRadius.circular(20),
maxHeight: MediaQuery.of(context).size.height * 0.75,
), ),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Row( // Warning icon
mainAxisAlignment: MainAxisAlignment.spaceBetween, Container(
children: [
Expanded(
child: Text(
paket['nama'],
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.navyBlue,
),
),
),
IconButton(
icon: Icon(Icons.close, color: AppColorsPetugas.blueGrotto),
onPressed: () => Navigator.pop(context),
),
],
),
const SizedBox(height: 16),
Expanded(
child: ListView(
children: [
Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( decoration: BoxDecoration(
crossAxisAlignment: CrossAxisAlignment.start, color: AppColorsPetugas.errorLight,
children: [ shape: BoxShape.circle,
_buildDetailItem('Kategori', paket['kategori']),
_buildDetailItem(
'Harga',
controller.formatPrice(paket['harga']),
), ),
_buildDetailItem( child: Icon(
'Status', Icons.delete_forever,
paket['tersedia'] ? 'Tersedia' : 'Tidak Tersedia', color: AppColorsPetugas.error,
), size: 32,
_buildDetailItem('Deskripsi', paket['deskripsi']),
],
), ),
), ),
),
const SizedBox(height: 16), const SizedBox(height: 24),
// Title and message
Text( Text(
'Item dalam Paket', 'Konfirmasi Hapus',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColorsPetugas.navyBlue, color: AppColorsPetugas.navyBlue,
), ),
textAlign: TextAlign.center,
), ),
const SizedBox(height: 8),
Card( const SizedBox(height: 12),
margin: EdgeInsets.zero,
child: ListView.separated( Text(
physics: const NeverScrollableScrollPhysics(), 'Apakah Anda yakin ingin menghapus paket "$nama"? Tindakan ini tidak dapat dibatalkan.',
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( style: TextStyle(
color: AppColorsPetugas.blueGrotto, fontSize: 14,
fontWeight: FontWeight.bold, color: AppColorsPetugas.textPrimary,
), ),
textAlign: TextAlign.center,
), ),
);
}, const SizedBox(height: 24),
),
), // Action buttons
],
),
),
const SizedBox(height: 16),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: OutlinedButton.icon( child: OutlinedButton(
onPressed: () { onPressed: () => Navigator.pop(context),
Navigator.pop(context);
Get.toNamed(
Routes.PETUGAS_TAMBAH_PAKET,
arguments: paket,
);
},
icon: const Icon(Icons.edit),
label: const Text('Edit'),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: AppColorsPetugas.blueGrotto, foregroundColor: AppColorsPetugas.textPrimary,
side: BorderSide(color: AppColorsPetugas.blueGrotto), side: BorderSide(color: AppColorsPetugas.divider),
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
), ),
), ),
child: const Text('Batal'),
),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: ElevatedButton.icon( child: ElevatedButton(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
_showDeleteConfirmation(context, paket); controller.deletePaket(id);
}, },
icon: const Icon(Icons.delete),
label: const Text('Hapus'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColorsPetugas.error, backgroundColor: AppColorsPetugas.error,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(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'), child: const Text('Hapus'),
), ),
),
], ],
),
],
),
),
); );
}, },
); );

View 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'),
),
],
),
);
}
}

View File

@ -6,6 +6,7 @@ import '../widgets/petugas_bumdes_bottom_navbar.dart';
import '../widgets/petugas_side_navbar.dart'; import '../widgets/petugas_side_navbar.dart';
import '../controllers/petugas_bumdes_dashboard_controller.dart'; import '../controllers/petugas_bumdes_dashboard_controller.dart';
import 'petugas_detail_sewa_view.dart'; import 'petugas_detail_sewa_view.dart';
import '../../../data/models/rental_booking_model.dart';
class PetugasSewaView extends StatefulWidget { class PetugasSewaView extends StatefulWidget {
const PetugasSewaView({Key? key}) : super(key: key); const PetugasSewaView({Key? key}) : super(key: key);
@ -160,6 +161,10 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
} }
Widget _buildSearchSection() { Widget _buildSearchSection() {
// Tambahkan controller untuk TextField agar bisa dikosongkan
final TextEditingController searchController = TextEditingController(
text: controller.searchQuery.value,
);
return Container( return Container(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -173,9 +178,9 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
], ],
), ),
child: TextField( child: TextField(
controller: searchController,
onChanged: (value) { onChanged: (value) {
controller.setSearchQuery(value); controller.setSearchQuery(value);
controller.setOrderIdQuery(value);
}, },
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Cari nama warga atau ID pesanan...', hintText: 'Cari nama warga atau ID pesanan...',
@ -204,11 +209,22 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
), ),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
isDense: true, isDense: true,
suffixIcon: Icon( suffixIcon: Obx(
Icons.tune_rounded, () =>
controller.searchQuery.value.isNotEmpty
? IconButton(
icon: Icon(
Icons.close,
color: AppColorsPetugas.textSecondary, color: AppColorsPetugas.textSecondary,
size: 20, size: 20,
), ),
onPressed: () {
searchController.clear();
controller.setSearchQuery('');
},
)
: SizedBox.shrink(),
),
), ),
), ),
); );
@ -241,17 +257,44 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
final filteredList = final filteredList =
status == 'Semua' status == 'Semua'
? controller.filteredSewaList ? controller.filteredSewaList
: status == 'Menunggu Pembayaran'
? controller.sewaList
.where(
(sewa) =>
sewa.status.toUpperCase() == 'MENUNGGU PEMBAYARAN' ||
sewa.status.toUpperCase() == 'PEMBAYARAN DENDA',
)
.toList()
: status == 'Periksa Pembayaran' : status == 'Periksa Pembayaran'
? controller.sewaList ? controller.sewaList
.where( .where(
(sewa) => (sewa) =>
sewa['status'] == 'Periksa Pembayaran' || sewa.status.toUpperCase() == 'PERIKSA PEMBAYARAN' ||
sewa['status'] == 'Pembayaran Denda' || sewa.status.toUpperCase() == 'PERIKSA PEMBAYARAN DENDA',
sewa['status'] == 'Periksa Denda',
) )
.toList() .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 : controller.sewaList
.where((sewa) => sewa['status'] == status) .where((sewa) => sewa.status == status)
.toList(); .toList();
if (filteredList.isEmpty) { if (filteredList.isEmpty) {
@ -313,40 +356,25 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
}); });
} }
Widget _buildSewaCard(BuildContext context, Map<String, dynamic> sewa) { Widget _buildSewaCard(BuildContext context, SewaModel sewa) {
final statusColor = controller.getStatusColor(sewa['status']); final statusColor = controller.getStatusColor(sewa.status);
final status = sewa['status']; final status = sewa.status;
// Get appropriate icon for status // Get appropriate icon for status
IconData statusIcon; IconData statusIcon = controller.getStatusIcon(status);
switch (status) {
case 'Menunggu Pembayaran': // Flag untuk membedakan tipe pesanan
statusIcon = Icons.payments_outlined; final bool isAset = sewa.tipePesanan == 'tunggal';
break; final bool isPaket = sewa.tipePesanan == 'paket';
case 'Periksa Pembayaran':
statusIcon = Icons.fact_check_outlined; // Pilih nama aset/paket
break; final String namaAsetAtauPaket =
case 'Diterima': isAset
statusIcon = Icons.check_circle_outlined; ? (sewa.asetNama ?? '-')
break; : (isPaket ? (sewa.paketNama ?? '-') : '-');
case 'Pembayaran Denda': // Pilih foto aset/paket jika ingin digunakan
statusIcon = Icons.money_off_csred_outlined; final String? fotoAsetAtauPaket =
break; isAset ? sewa.asetFoto : (isPaket ? sewa.paketFoto : null);
case 'Periksa Denda':
statusIcon = Icons.assignment_late_outlined;
break;
case 'Dikembalikan':
statusIcon = Icons.assignment_return_outlined;
break;
case 'Selesai':
statusIcon = Icons.task_alt_outlined;
break;
case 'Dibatalkan':
statusIcon = Icons.cancel_outlined;
break;
default:
statusIcon = Icons.help_outline_rounded;
}
return Container( return Container(
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
@ -370,6 +398,35 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
child: Row( child: Row(
@ -378,14 +435,22 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
CircleAvatar( CircleAvatar(
radius: 24, radius: 24,
backgroundColor: AppColorsPetugas.babyBlueLight, backgroundColor: AppColorsPetugas.babyBlueLight,
child: Text( backgroundImage:
sewa['nama_warga'].substring(0, 1).toUpperCase(), (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( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColorsPetugas.blueGrotto, color: AppColorsPetugas.blueGrotto,
), ),
), )
: null,
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
@ -395,61 +460,29 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
sewa['nama_warga'], sewa.wargaNama,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColorsPetugas.textPrimary, 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( Text(
status, 'Tanggal Pesan: ' +
(sewa.tanggalPemesanan != null
? '${sewa.tanggalPemesanan.day.toString().padLeft(2, '0')}-${sewa.tanggalPemesanan.month.toString().padLeft(2, '0')}-${sewa.tanggalPemesanan.year}'
: '-'),
style: TextStyle( style: TextStyle(
fontSize: 12, 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, color: AppColorsPetugas.textSecondary,
), ),
), ),
], ],
), ),
],
),
), ),
// Price // Price - only show if total_tagihan > 0
if (sewa.totalTagihan > 0)
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, horizontal: 10,
@ -460,7 +493,7 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
controller.formatPrice(sewa['total_biaya']), controller.formatPrice(sewa.totalTagihan),
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -481,12 +514,30 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
child: Divider(height: 1, color: Colors.grey.shade200), child: Divider(height: 1, color: Colors.grey.shade200),
), ),
// Asset details // Asset/Paket details
Padding( Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16), padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
child: Row( child: Row(
children: [ children: [
// Asset icon // 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( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -501,13 +552,13 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
// Asset name and duration // Asset/Paket name and duration
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
sewa['nama_aset'], namaAsetAtauPaket,
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -524,7 +575,7 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'${sewa['tanggal_mulai']} - ${sewa['tanggal_selesai']}', _formatDateRange(sewa),
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: AppColorsPetugas.textSecondary, 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() { void _showFilterBottomSheet() {
Get.bottomSheet( Get.bottomSheet(
Container( Container(

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../theme/app_colors_petugas.dart'; import '../../../theme/app_colors_petugas.dart';
@ -9,32 +10,51 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Obx(() => Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: AppBar( appBar: AppBar(
title: const Text( title: Text(
'Tambah Aset', controller.isEditing.value ? 'Edit Aset' : 'Tambah Aset',
style: TextStyle(fontWeight: FontWeight.w600), style: const TextStyle(fontWeight: FontWeight.w600),
), ),
backgroundColor: AppColorsPetugas.navyBlue, backgroundColor: AppColorsPetugas.navyBlue,
elevation: 0, elevation: 0,
centerTitle: true, centerTitle: true,
), ),
body: SafeArea( body: Stack(
children: [
SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [_buildHeaderSection(), _buildFormSection(context)], children: [
_buildHeaderSection(),
_buildFormSection(context),
],
), ),
), ),
), ),
if (controller.isLoading.value)
Container(
color: Colors.black54,
child: const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(AppColorsPetugas.blueGrotto),
),
),
),
],
),
bottomNavigationBar: _buildBottomBar(), bottomNavigationBar: _buildBottomBar(),
)),
); );
} }
Widget _buildHeaderSection() { Widget _buildHeaderSection() {
return Container( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.only(top: 10, left: 20, right: 20, bottom: 5), // Reduced padding
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto], colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto],
@ -42,50 +62,8 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
), ),
), ),
child: Column( child: Container(
crossAxisAlignment: CrossAxisAlignment.start, height: 12, // Further reduced height
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),
),
),
],
),
),
],
),
],
), ),
); );
} }
@ -131,49 +109,29 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
_buildImageUploader(), _buildImageUploader(),
const SizedBox(height: 24), const SizedBox(height: 24),
// Category Section // Status Section
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'), _buildSectionHeader(icon: Icons.check_circle, title: 'Status'),
const SizedBox(height: 16), const SizedBox(height: 16),
// Category and Status as cards // Status card
Row( _buildCategorySelect(
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', title: 'Status',
options: controller.statusOptions, options: controller.statusOptions,
selectedOption: controller.selectedStatus, selectedOption: controller.selectedStatus,
onChanged: controller.setStatus, onChanged: controller.setStatus,
icon: Icons.check_circle, icon: Icons.check_circle,
), ),
),
],
),
const SizedBox(height: 24), const SizedBox(height: 24),
// Quantity Section // Quantity Section
_buildSectionHeader( _buildSectionHeader(
icon: Icons.format_list_numbered, icon: Icons.format_list_numbered,
title: 'Kuantitas & Pengukuran', title: 'Kuantitas',
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Quantity fields // Quantity field
Row( _buildTextField(
children: [
Expanded(
flex: 2,
child: _buildTextField(
label: 'Kuantitas', label: 'Kuantitas',
hint: 'Jumlah aset', hint: 'Jumlah aset',
controller: controller.quantityController, controller: controller.quantityController,
@ -182,19 +140,6 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
inputFormatters: [FilteringTextInputFormatter.digitsOnly], inputFormatters: [FilteringTextInputFormatter.digitsOnly],
prefixIcon: Icons.numbers, prefixIcon: Icons.numbers,
), ),
),
const SizedBox(width: 12),
Expanded(
flex: 3,
child: _buildTextField(
label: 'Satuan Ukur',
hint: 'contoh: Unit, Buah',
controller: controller.unitOfMeasureController,
prefixIcon: Icons.straighten,
),
),
],
),
const SizedBox(height: 24), const SizedBox(height: 24),
// Rental Options Section // Rental Options Section
@ -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() { Widget _buildImageUploader() {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -696,7 +749,7 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
children: [ children: [
// Add button // Add button
GestureDetector( GestureDetector(
onTap: () => controller.addSampleImage(), onTap: _showImageSourceOptions,
child: Container( child: Container(
width: 100, width: 100,
height: 100, height: 100,
@ -732,37 +785,76 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
), ),
// Image previews // Image previews
...controller.selectedImages.asMap().entries.map((entry) { ...List<Widget>.generate(
final index = entry.key; controller.selectedImages.length,
return Container( (index) => Stack(
children: [
Container(
width: 100, width: 100,
height: 100, height: 100,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueLight,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
boxShadow: [ border: Border.all(color: Colors.grey[300]!),
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
), ),
], 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),
);
},
), ),
child: Stack( );
children: [ } else {
ClipRRect( // Display local file
borderRadius: BorderRadius.circular(10), return ClipRRect(
child: Container( borderRadius: BorderRadius.circular(8),
width: 100, child: FutureBuilder<File>(
height: 100, future: File(controller.selectedImages[index].path).exists().then((exists) {
color: AppColorsPetugas.babyBlueLight, if (exists) {
child: Center( return File(controller.selectedImages[index].path);
child: Icon( } else {
Icons.image, return File(controller.selectedImages[index].path);
color: AppColorsPetugas.blueGrotto, }
size: 40, }),
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( Positioned(
@ -771,30 +863,29 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
child: GestureDetector( child: GestureDetector(
onTap: () => controller.removeImage(index), onTap: () => controller.removeImage(index),
child: Container( child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(2),
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.white, color: Colors.white,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.1), color: Colors.black26,
blurRadius: 3, blurRadius: 4,
offset: const Offset(0, 1), offset: Offset(0, 1),
), ),
], ],
), ),
child: Icon( child: const Icon(
Icons.close, Icons.close,
color: AppColorsPetugas.error,
size: 16, size: 16,
color: Colors.red,
), ),
), ),
), ),
), ),
], ],
), ),
); ).toList(),
}),
], ],
), ),
), ),
@ -850,7 +941,9 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
), ),
) )
: const Icon(Icons.save), : 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( style: ElevatedButton.styleFrom(
backgroundColor: AppColorsPetugas.blueGrotto, backgroundColor: AppColorsPetugas.blueGrotto,
foregroundColor: Colors.white, foregroundColor: Colors.white,

View File

@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../theme/app_colors_petugas.dart'; import '../../../theme/app_colors_petugas.dart';
import '../controllers/petugas_tambah_paket_controller.dart'; import '../controllers/petugas_tambah_paket_controller.dart';
import 'dart:io';
class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> { class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
const PetugasTambahPaketView({Key? key}) : super(key: key); const PetugasTambahPaketView({Key? key}) : super(key: key);
@ -12,9 +13,13 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: AppBar( appBar: AppBar(
title: const Text( title: Obx(
'Tambah Paket', () => Text(
style: TextStyle(fontWeight: FontWeight.w600), controller.isViewing.value
? 'Detail Paket'
: (controller.isEditing.value ? 'Edit Paket' : 'Tambah Paket'),
style: const TextStyle(fontWeight: FontWeight.w600),
),
), ),
backgroundColor: AppColorsPetugas.navyBlue, backgroundColor: AppColorsPetugas.navyBlue,
elevation: 0, elevation: 0,
@ -24,7 +29,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [_buildHeaderSection(), _buildFormSection(context)], children: [_buildFormSection(context)],
), ),
), ),
), ),
@ -32,64 +37,6 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
); );
} }
Widget _buildHeaderSection() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.category,
color: Colors.white,
size: 28,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informasi Paket Baru',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
'Isi data dengan lengkap untuk menambahkan paket',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.8),
),
),
],
),
),
],
),
],
),
);
}
Widget _buildFormSection(BuildContext context) { Widget _buildFormSection(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
@ -132,22 +79,22 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
const SizedBox(height: 24), const SizedBox(height: 24),
// Category Section // Category Section
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'), _buildSectionHeader(icon: Icons.category, title: 'Status'),
const SizedBox(height: 16), const SizedBox(height: 16),
// Category and Status as cards // Category and Status as cards
Row( Row(
children: [ children: [
Expanded( // Expanded(
child: _buildCategorySelect( // child: _buildCategorySelect(
title: 'Kategori', // title: 'Kategori',
options: controller.categoryOptions, // options: controller.categoryOptions,
selectedOption: controller.selectedCategory, // selectedOption: controller.selectedCategory,
onChanged: controller.setCategory, // onChanged: controller.setCategory,
icon: Icons.category, // icon: Icons.category,
), // ),
), // ),
const SizedBox(width: 12), // const SizedBox(width: 12),
Expanded( Expanded(
child: _buildCategorySelect( child: _buildCategorySelect(
title: 'Status', title: 'Status',
@ -161,24 +108,6 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Price Section
_buildSectionHeader(
icon: Icons.monetization_on,
title: 'Harga Paket',
),
const SizedBox(height: 16),
_buildTextField(
label: 'Harga Paket',
hint: 'Masukkan harga paket',
controller: controller.priceController,
isRequired: true,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
prefixText: 'Rp ',
prefixIcon: Icons.payments,
),
const SizedBox(height: 24),
// Package Items Section // Package Items Section
_buildSectionHeader( _buildSectionHeader(
icon: Icons.inventory_2, icon: Icons.inventory_2,
@ -186,6 +115,40 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildPackageItems(), _buildPackageItems(),
const SizedBox(height: 24),
_buildSectionHeader(
icon: Icons.schedule,
title: 'Opsi Waktu & Harga Sewa',
),
const SizedBox(height: 16),
_buildTimeOptionsCards(),
const SizedBox(height: 16),
Obx(
() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (controller.timeOptions['Per Jam']!.value)
_buildPriceCard(
title: 'Harga Per Jam',
icon: Icons.timer,
priceController: controller.pricePerHourController,
maxController: controller.maxHourController,
maxLabel: 'Maksimal Jam',
),
if (controller.timeOptions['Per Jam']!.value &&
controller.timeOptions['Per Hari']!.value)
const SizedBox(height: 16),
if (controller.timeOptions['Per Hari']!.value)
_buildPriceCard(
title: 'Harga Per Hari',
icon: Icons.calendar_today,
priceController: controller.pricePerDayController,
maxController: controller.maxDayController,
maxLabel: 'Maksimal Hari',
),
],
),
),
const SizedBox(height: 40), const SizedBox(height: 40),
], ],
), ),
@ -207,6 +170,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
'Item Paket', 'Item Paket',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
if (!controller.isViewing.value)
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () => _showAddItemDialog(), onPressed: () => _showAddItemDialog(),
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
@ -247,7 +211,10 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
Text('Stok tersedia: ${item['stok']}'), Text('Stok tersedia: ${item['stok']}'),
], ],
), ),
trailing: Row( trailing:
controller.isViewing.value
? null
: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( IconButton(
@ -255,7 +222,9 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
Icons.edit, Icons.edit,
color: Colors.blue, color: Colors.blue,
), ),
onPressed: () => _showEditItemDialog(index), onPressed:
() =>
_showEditItemDialog(index),
), ),
IconButton( IconButton(
icon: const Icon( icon: const Icon(
@ -263,7 +232,9 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
color: Colors.red, color: Colors.red,
), ),
onPressed: onPressed:
() => controller.removeItem(index), () => controller.removeItem(
index,
),
), ),
], ],
), ),
@ -310,7 +281,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
const SizedBox(height: 16), const SizedBox(height: 16),
// Asset dropdown // Asset dropdown
DropdownButtonFormField<int>( DropdownButtonFormField<String>(
value: controller.selectedAsset.value, value: controller.selectedAsset.value,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Pilih Aset', labelText: 'Pilih Aset',
@ -319,8 +290,8 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
hint: const Text('Pilih Aset'), hint: const Text('Pilih Aset'),
items: items:
controller.availableAssets.map((asset) { controller.availableAssets.map((asset) {
return DropdownMenuItem<int>( return DropdownMenuItem<String>(
value: asset['id'] as int, value: asset['id'].toString(),
child: Text( child: Text(
'${asset['nama']} (Stok: ${asset['stok']})', '${asset['nama']} (Stok: ${asset['stok']})',
), ),
@ -422,7 +393,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
const SizedBox(height: 16), const SizedBox(height: 16),
// Asset dropdown // Asset dropdown
DropdownButtonFormField<int>( DropdownButtonFormField<String>(
value: controller.selectedAsset.value, value: controller.selectedAsset.value,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Pilih Aset', labelText: 'Pilih Aset',
@ -431,8 +402,8 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
hint: const Text('Pilih Aset'), hint: const Text('Pilih Aset'),
items: items:
controller.availableAssets.map((asset) { controller.availableAssets.map((asset) {
return DropdownMenuItem<int>( return DropdownMenuItem<String>(
value: asset['id'] as int, value: asset['id'].toString(),
child: Text( child: Text(
'${asset['nama']} (Stok: ${asset['stok']})', '${asset['nama']} (Stok: ${asset['stok']})',
), ),
@ -565,6 +536,9 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
String? prefixText, String? prefixText,
IconData? prefixIcon, IconData? prefixIcon,
}) { }) {
final petugasController =
this.controller; // Reference to PetugasTambahPaketController
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -578,7 +552,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
color: AppColorsPetugas.textPrimary, color: AppColorsPetugas.textPrimary,
), ),
), ),
if (isRequired) ...[ if (isRequired && !petugasController.isViewing.value) ...[
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'*', '*',
@ -592,16 +566,27 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
TextFormField( Obx(
() => TextFormField(
controller: controller, controller: controller,
maxLines: maxLines, maxLines: maxLines,
keyboardType: keyboardType, keyboardType: keyboardType,
inputFormatters: inputFormatters, inputFormatters: inputFormatters,
readOnly: petugasController.isViewing.value,
style: TextStyle(
color:
petugasController.isViewing.value
? AppColorsPetugas.textSecondary
: AppColorsPetugas.textPrimary,
),
decoration: InputDecoration( decoration: InputDecoration(
hintText: hint, hintText: hint,
hintStyle: TextStyle(color: AppColorsPetugas.textLight), hintStyle: TextStyle(color: AppColorsPetugas.textLight),
filled: true, filled: true,
fillColor: AppColorsPetugas.babyBlueBright, fillColor:
petugasController.isViewing.value
? AppColorsPetugas.babyBlueLight
: AppColorsPetugas.babyBlueBright,
prefixText: prefixText, prefixText: prefixText,
prefixIcon: prefixIcon:
prefixIcon != null prefixIcon != null
@ -632,6 +617,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
), ),
), ),
), ),
),
], ],
); );
} }
@ -685,7 +671,10 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
vertical: 12, vertical: 12,
), ),
filled: true, filled: true,
fillColor: AppColorsPetugas.babyBlueBright, fillColor:
controller.isViewing.value
? AppColorsPetugas.babyBlueLight
: AppColorsPetugas.babyBlueBright,
), ),
items: items:
options.map((option) { options.map((option) {
@ -700,7 +689,10 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
), ),
); );
}).toList(), }).toList(),
onChanged: (value) { onChanged:
controller.isViewing.value
? null // Disable in view-only mode
: (value) {
if (value != null) onChanged(value); if (value != null) onChanged(value);
}, },
icon: Icon( icon: Icon(
@ -756,8 +748,9 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
runSpacing: 12, runSpacing: 12,
children: [ children: [
// Add button // Add button
if (!controller.isViewing.value)
GestureDetector( GestureDetector(
onTap: () => controller.addSampleImage(), onTap: _showImageSourceOptions,
child: Container( child: Container(
width: 100, width: 100,
height: 100, height: 100,
@ -791,37 +784,58 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
), ),
), ),
), ),
// Image previews // Image previews
...controller.selectedImages.asMap().entries.map((entry) { ...List<Widget>.generate(controller.selectedImages.length, (
final index = entry.key; index,
return Container( ) {
final img = controller.selectedImages[index];
return Stack(
children: [
Container(
width: 100, width: 100,
height: 100, height: 100,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueLight,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
boxShadow: [ border: Border.all(color: Colors.grey[300]!),
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
), ),
], child: ClipRRect(
), borderRadius: BorderRadius.circular(8),
child: Stack( child:
children: [ (img is String && img.startsWith('http'))
ClipRRect( ? Image.network(
borderRadius: BorderRadius.circular(10), img,
child: Container( fit: BoxFit.cover,
width: 100, width: double.infinity,
height: 100, height: double.infinity,
color: AppColorsPetugas.babyBlueLight, errorBuilder:
child: Center( (context, error, stackTrace) =>
const Center(
child: Icon( child: Icon(
Icons.image, Icons.broken_image,
color: AppColorsPetugas.blueGrotto, color: Colors.grey,
size: 40, ),
),
)
: (img is String)
? Container(
color: Colors.grey[200],
child: const Icon(
Icons.broken_image,
color: Colors.grey,
),
)
: Image.file(
File(img.path),
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
errorBuilder:
(context, error, stackTrace) =>
const Center(
child: Icon(
Icons.broken_image,
color: Colors.grey,
),
), ),
), ),
), ),
@ -829,35 +843,128 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
Positioned( Positioned(
top: 4, top: 4,
right: 4, right: 4,
child: GestureDetector( child:
!controller.isViewing.value
? InkWell(
onTap: () => controller.removeImage(index), onTap: () => controller.removeImage(index),
child: Container( child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.white, color: Colors.white,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ ),
BoxShadow( child: const Icon(
color: Colors.black.withOpacity(0.1), Icons.close,
blurRadius: 3, size: 18,
offset: const Offset(0, 1), color: Colors.red,
),
),
)
: const SizedBox.shrink(),
), ),
], ],
), );
child: Icon( }),
Icons.close, ],
color: AppColorsPetugas.error,
size: 16,
),
),
), ),
), ),
], ],
), ),
); );
}), }
void _showImageSourceOptions() {
Get.bottomSheet(
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, -2),
),
], ],
), ),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
Text(
'Pilih Sumber Gambar',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.navyBlue,
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildImageSourceOption(
icon: Icons.camera_alt,
label: 'Kamera',
onTap: () {
Get.back();
controller.pickImageFromCamera();
},
),
_buildImageSourceOption(
icon: Icons.photo_library,
label: 'Galeri',
onTap: () {
Get.back();
controller.pickImageFromGallery();
},
),
],
),
const SizedBox(height: 10),
],
),
),
);
}
Widget _buildImageSourceOption({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Column(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueBright,
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: AppColorsPetugas.blueGrotto, size: 28),
),
const SizedBox(height: 8),
Text(
label,
style: TextStyle(
fontSize: 14,
color: AppColorsPetugas.navyBlue,
fontWeight: FontWeight.w500,
),
), ),
], ],
), ),
@ -877,7 +984,33 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
), ),
], ],
), ),
child: Row( child: Obx(() {
// In view-only mode, just show a back button
if (controller.isViewing.value) {
return SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => Get.back(),
icon: const Icon(Icons.arrow_back),
label: const Text('Kembali'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColorsPetugas.blueGrotto,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
textStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
);
}
// For edit/add mode, show cancel and save buttons
return Row(
children: [ children: [
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () => Get.back(), onPressed: () => Get.back(),
@ -885,7 +1018,10 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
label: const Text('Batal'), label: const Text('Batal'),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: AppColorsPetugas.textSecondary, foregroundColor: AppColorsPetugas.textSecondary,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
side: BorderSide(color: AppColorsPetugas.divider), side: BorderSide(color: AppColorsPetugas.divider),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
@ -899,26 +1035,37 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
final isSubmitting = controller.isSubmitting.value; final isSubmitting = controller.isSubmitting.value;
return ElevatedButton.icon( return ElevatedButton.icon(
onPressed: onPressed:
isValid && !isSubmitting ? controller.savePaket : null, controller.isFormChanged.value && !isSubmitting
? controller.savePaket
: null,
icon: icon:
isSubmitting isSubmitting
? SizedBox( ? const SizedBox(
height: 20, width: 24,
width: 20, height: 24,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
color: Colors.white, color: Colors.white,
), ),
) )
: const Icon(Icons.save), : const Icon(Icons.save),
label: Text(isSubmitting ? 'Menyimpan...' : 'Simpan Paket'), label: Text(
isSubmitting
? 'Menyimpan...'
: (controller.isEditing.value
? 'Simpan Paket'
: 'Tambah Paket'),
),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColorsPetugas.blueGrotto, backgroundColor: AppColorsPetugas.blueGrotto,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 16),
elevation: 0, textStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(12),
), ),
disabledBackgroundColor: AppColorsPetugas.textLight, disabledBackgroundColor: AppColorsPetugas.textLight,
), ),
@ -926,6 +1073,252 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
}), }),
), ),
], ],
);
}),
);
}
Widget _buildTimeOptionsCards() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children:
controller.timeOptions.entries.map((entry) {
final option = entry.key;
final isSelected = entry.value;
return Obx(
() => Material(
color: Colors.transparent,
child: InkWell(
onTap:
controller.isViewing.value
? null
: () => controller.toggleTimeOption(option),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color:
isSelected.value
? AppColorsPetugas.blueGrotto.withOpacity(
0.1,
)
: Colors.grey.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
option == 'Per Jam'
? Icons.hourglass_bottom
: Icons.calendar_today,
color:
isSelected.value
? AppColorsPetugas.blueGrotto
: Colors.grey,
size: 20,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
option,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color:
isSelected.value
? AppColorsPetugas.navyBlue
: Colors.grey.shade700,
),
),
const SizedBox(height: 2),
Text(
option == 'Per Jam'
? 'Sewa paket dengan basis perhitungan per jam'
: 'Sewa paket dengan basis perhitungan per hari',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
Checkbox(
value: isSelected.value,
onChanged:
(_) => controller.toggleTimeOption(option),
activeColor: AppColorsPetugas.blueGrotto,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
),
),
);
}).toList(),
),
);
}
Widget _buildPriceCard({
required String title,
required IconData icon,
required TextEditingController priceController,
required TextEditingController maxController,
required String maxLabel,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColorsPetugas.babyBlue.withOpacity(0.5)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 20, color: AppColorsPetugas.blueGrotto),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColorsPetugas.navyBlue,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Harga Sewa',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColorsPetugas.textSecondary,
),
),
const SizedBox(height: 8),
TextFormField(
controller: priceController,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
readOnly: controller.isViewing.value,
style: TextStyle(
color:
controller.isViewing.value
? AppColorsPetugas.textSecondary
: AppColorsPetugas.textPrimary,
),
decoration: InputDecoration(
hintText: 'Masukkan harga',
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
prefixText: 'Rp ',
filled: true,
fillColor:
controller.isViewing.value
? AppColorsPetugas.babyBlueLight
: AppColorsPetugas.babyBlueBright,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
),
),
],
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
maxLabel,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColorsPetugas.textSecondary,
),
),
const SizedBox(height: 8),
TextFormField(
controller: maxController,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
readOnly: controller.isViewing.value,
style: TextStyle(
color:
controller.isViewing.value
? AppColorsPetugas.textSecondary
: AppColorsPetugas.textPrimary,
),
decoration: InputDecoration(
hintText: 'Opsional',
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
filled: true,
fillColor:
controller.isViewing.value
? AppColorsPetugas.babyBlueLight
: AppColorsPetugas.babyBlueBright,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
),
),
],
),
),
],
),
],
), ),
); );
} }

View File

@ -64,6 +64,14 @@ class PetugasBumdesBottomNavbar extends StatelessWidget {
isSelected: selectedIndex == 3, isSelected: selectedIndex == 3,
onTap: () => onItemTapped(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, required VoidCallback onTap,
}) { }) {
final primaryColor = AppColors.primary; final primaryColor = AppColors.primary;
final tabWidth = MediaQuery.of(context).size.width / 4; final tabWidth = MediaQuery.of(context).size.width / 5;
return Material( return Material(
color: Colors.transparent, color: Colors.transparent,

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../theme/app_colors.dart'; import '../../../theme/app_colors.dart';
import '../../../theme/app_colors_petugas.dart';
import '../controllers/petugas_bumdes_dashboard_controller.dart'; import '../controllers/petugas_bumdes_dashboard_controller.dart';
import '../../../routes/app_routes.dart';
class PetugasSideNavbar extends StatelessWidget { class PetugasSideNavbar extends StatelessWidget {
final PetugasBumdesDashboardController controller; final PetugasBumdesDashboardController controller;
@ -11,7 +13,7 @@ class PetugasSideNavbar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Drawer( return Drawer(
backgroundColor: Colors.white, backgroundColor: AppColorsPetugas.babyBlueLight,
elevation: 0, elevation: 0,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
@ -32,14 +34,17 @@ class PetugasSideNavbar extends StatelessWidget {
Widget _buildHeader() { Widget _buildHeader() {
return Container( return Container(
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20), padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
color: AppColors.primary, color: AppColorsPetugas.navyBlue,
width: double.infinity, width: double.infinity,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
Container( Obx(() {
final avatar = controller.avatarUrl.value;
if (avatar.isNotEmpty) {
return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2), border: Border.all(color: Colors.white, width: 2),
@ -47,9 +52,28 @@ class PetugasSideNavbar extends StatelessWidget {
child: CircleAvatar( child: CircleAvatar(
radius: 30, radius: 30,
backgroundColor: Colors.white, backgroundColor: Colors.white,
child: Icon(Icons.person, color: AppColors.primary, size: 36), 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), const SizedBox(width: 16),
Expanded( Expanded(
child: Column( child: Column(
@ -125,6 +149,30 @@ class PetugasSideNavbar extends StatelessWidget {
isSelected: controller.currentTabIndex.value == 3, isSelected: controller.currentTabIndex.value == 3,
onTap: () => controller.changeTab(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),
),
], ],
), ),
); );

View File

@ -41,7 +41,7 @@ class PetugasMitraDashboardController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal keluar dari aplikasi', 'Gagal keluar dari aplikasi',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }

View File

@ -34,7 +34,7 @@ class SplashView extends GetView<SplashController> {
child: Container( child: Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
image: DecorationImage( image: DecorationImage(
image: AssetImage('assets/images/pattern.png'), image: AssetImage('assets/images/logo.png'), // Using logo.png which exists
repeat: ImageRepeat.repeat, repeat: ImageRepeat.repeat,
scale: 4.0, scale: 4.0,
), ),

View File

@ -1,7 +1,6 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/order_sewa_paket_controller.dart'; import '../controllers/order_sewa_paket_controller.dart';
import '../../../data/providers/aset_provider.dart'; import '../../../data/providers/aset_provider.dart';
import '../../../data/providers/sewa_provider.dart';
class OrderSewaPaketBinding extends Bindings { class OrderSewaPaketBinding extends Bindings {
@override @override
@ -11,10 +10,6 @@ class OrderSewaPaketBinding extends Bindings {
Get.put(AsetProvider()); Get.put(AsetProvider());
} }
if (!Get.isRegistered<SewaProvider>()) {
Get.put(SewaProvider());
}
Get.lazyPut<OrderSewaPaketController>( Get.lazyPut<OrderSewaPaketController>(
() => OrderSewaPaketController(), () => OrderSewaPaketController(),
); );

View File

@ -8,12 +8,6 @@ import '../../../data/providers/aset_provider.dart';
class WargaSewaBinding extends Bindings { class WargaSewaBinding extends Bindings {
@override @override
void dependencies() { 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 // Ensure AuthProvider is registered
if (!Get.isRegistered<AuthProvider>()) { if (!Get.isRegistered<AuthProvider>()) {
Get.put(AuthProvider(), permanent: true); Get.put(AuthProvider(), permanent: true);

View File

@ -378,7 +378,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Pesanan Dibatalkan', 'Pesanan Dibatalkan',
'Batas waktu pembayaran telah berakhir', 'Batas waktu pembayaran telah berakhir',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
duration: Duration(seconds: 5), duration: Duration(seconds: 5),
@ -417,7 +417,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal mengambil foto: ${e.toString()}', 'Gagal mengambil foto: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -443,7 +443,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memilih foto dari galeri: ${e.toString()}', 'Gagal memilih foto dari galeri: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -459,7 +459,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Mohon unggah bukti pembayaran terlebih dahulu', 'Mohon unggah bukti pembayaran terlebih dahulu',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -541,7 +541,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Bukti pembayaran berhasil diunggah', 'Bukti pembayaran berhasil diunggah',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -550,7 +550,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal mengunggah bukti pembayaran: ${e.toString()}', 'Gagal mengunggah bukti pembayaran: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -654,7 +654,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Pembayaran tunai berhasil disubmit', 'Pembayaran tunai berhasil disubmit',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -1090,7 +1090,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Berhasil', 'Berhasil',
'Data berhasil diperbarui', 'Data berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
@ -1104,7 +1104,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memperbarui data', 'Gagal memperbarui data',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );

View File

@ -47,17 +47,28 @@ class PembayaranSewaController extends GetxController
final isLoading = false.obs; final isLoading = false.obs;
final currentStep = 0.obs; final currentStep = 0.obs;
// Payment proof images - now a list to support multiple images (both File and WebImageFile) // Payment proof images for tagihan awal
final RxList<dynamic> paymentProofImages = <dynamic>[].obs; final RxList<dynamic> paymentProofImagesTagihanAwal = <dynamic>[].obs;
// Payment proof images for denda
final RxList<dynamic> paymentProofImagesDenda = <dynamic>[].obs;
// Track original images loaded from database // Track original images loaded from database
final RxList<WebImageFile> originalImages = <WebImageFile>[].obs; final RxList<WebImageFile> originalImages = <WebImageFile>[].obs;
// Track images marked for deletion // Track images marked for deletion
final RxList<WebImageFile> imagesToDelete = <WebImageFile>[].obs; final RxList<WebImageFile> imagesToDeleteTagihanAwal = <WebImageFile>[].obs;
final RxList<WebImageFile> imagesToDeleteDenda = <WebImageFile>[].obs;
// Package related properties
final isPaket = false.obs;
final paketId = ''.obs;
final paketDetails = Rx<Map<String, dynamic>>({});
final paketItems = <Map<String, dynamic>>[].obs;
final isPaketItemsLoaded = false.obs;
// Flag to track if there are changes that need to be saved // Flag to track if there are changes that need to be saved
final RxBool hasUnsavedChanges = false.obs; final RxBool hasUnsavedChangesTagihanAwal = false.obs;
final RxBool hasUnsavedChangesDenda = false.obs;
// Get image widget for a specific image // Get image widget for a specific image
Widget getImageWidget(dynamic imageFile) { Widget getImageWidget(dynamic imageFile) {
@ -98,12 +109,7 @@ class PembayaranSewaController extends GetxController
} }
// For mobile with a File object // For mobile with a File object
else if (imageFile is File) { else if (imageFile is File) {
return Image.file( return Image.file(imageFile, height: 120, width: 120, fit: BoxFit.cover);
imageFile,
height: 120,
width: 120,
fit: BoxFit.cover,
);
} }
// Fallback for any other type // Fallback for any other type
else { else {
@ -118,18 +124,26 @@ class PembayaranSewaController extends GetxController
// Remove an image from the list // Remove an image from the list
void removeImage(dynamic image) { void removeImage(dynamic image) {
// If this is an existing image (WebImageFile), add it to imagesToDelete if (selectedPaymentType.value == 'denda') {
// Untuk denda
if (image is WebImageFile && image.id.isNotEmpty) { if (image is WebImageFile && image.id.isNotEmpty) {
imagesToDelete.add(image); imagesToDeleteDenda.add(image);
debugPrint('🗑️ Marked image for deletion: ${image.imageUrl} (ID: ${image.id})'); debugPrint(
'🗑️ Marked image for deletion (denda): \\${image.imageUrl} (ID: \\${image.id})',
);
}
paymentProofImagesDenda.remove(image);
} else {
// Default/tagihan awal
if (image is WebImageFile && image.id.isNotEmpty) {
imagesToDeleteTagihanAwal.add(image);
debugPrint(
'🗑️ Marked image for deletion: \\${image.imageUrl} (ID: \\${image.id})',
);
}
paymentProofImagesTagihanAwal.remove(image);
} }
// Remove from the current list
paymentProofImages.remove(image);
// Check if we have any changes (additions or deletions)
_checkForChanges(); _checkForChanges();
update(); update();
} }
@ -161,14 +175,17 @@ class PembayaranSewaController extends GetxController
panEnabled: true, panEnabled: true,
minScale: 0.5, minScale: 0.5,
maxScale: 4, maxScale: 4,
child: kIsWeb child:
kIsWeb
? Image.network( ? Image.network(
imageUrl, imageUrl,
fit: BoxFit.contain, fit: BoxFit.contain,
height: Get.height, height: Get.height,
width: Get.width, width: Get.width,
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
return const Center(child: Text('Error loading image')); return const Center(
child: Text('Error loading image'),
);
}, },
) )
: Image.file( : Image.file(
@ -196,35 +213,33 @@ class PembayaranSewaController extends GetxController
// Check if there are any changes to save (new images added or existing images removed) // Check if there are any changes to save (new images added or existing images removed)
void _checkForChanges() { void _checkForChanges() {
// We have changes if: bool hasChangesTagihanAwal = false;
// 1. We have images marked for deletion bool hasChangesDenda = false;
// 2. We have new images (files) added if (imagesToDeleteTagihanAwal.isNotEmpty) {
// 3. The current list differs from the original list hasChangesTagihanAwal = true;
bool hasChanges = false;
// Check if any images are marked for deletion
if (imagesToDelete.isNotEmpty) {
hasChanges = true;
} }
if (imagesToDeleteDenda.isNotEmpty) {
// Check if any new images have been added hasChangesDenda = true;
for (dynamic image in paymentProofImages) { }
for (dynamic image in paymentProofImagesTagihanAwal) {
if (image is File) { if (image is File) {
// This is a new image hasChangesTagihanAwal = true;
hasChanges = true;
break; break;
} }
} }
for (dynamic image in paymentProofImagesDenda) {
// Check if the number of images has changed if (image is File) {
if (paymentProofImages.length != originalImages.length) { hasChangesDenda = true;
hasChanges = true; break;
}
}
hasUnsavedChangesTagihanAwal.value = hasChangesTagihanAwal;
hasUnsavedChangesDenda.value = hasChangesDenda;
debugPrint(
'💾 Has unsaved changes (tagihan awal): $hasChangesTagihanAwal, (denda): $hasChangesDenda',
);
} }
hasUnsavedChanges.value = hasChanges;
debugPrint('💾 Has unsaved changes: $hasChanges');
}
final isUploading = false.obs; final isUploading = false.obs;
final uploadProgress = 0.0.obs; final uploadProgress = 0.0.obs;
@ -247,6 +262,24 @@ class PembayaranSewaController extends GetxController
if (Get.arguments['orderId'] != null) { if (Get.arguments['orderId'] != null) {
orderId.value = Get.arguments['orderId']; orderId.value = Get.arguments['orderId'];
// Get isPaket flag and paketId
isPaket.value = Get.arguments['isPaket'] == true;
if (isPaket.value && Get.arguments['paketId'] != null) {
paketId.value = Get.arguments['paketId'];
debugPrint(
'📦 This is a package order with paketId: ${paketId.value}',
);
}
// Set initial tab if specified
if (Get.arguments['initialTab'] != null) {
int initialTab = Get.arguments['initialTab'];
if (initialTab >= 0 && initialTab < tabController.length) {
debugPrint('Setting initial tab to: $initialTab');
tabController.animateTo(initialTab);
}
}
// If rental data is passed, use it directly // If rental data is passed, use it directly
if (Get.arguments['rentalData'] != null) { if (Get.arguments['rentalData'] != null) {
Map<String, dynamic> rentalData = Get.arguments['rentalData']; Map<String, dynamic> rentalData = Get.arguments['rentalData'];
@ -260,10 +293,21 @@ class PembayaranSewaController extends GetxController
'rental_period': rentalData['waktuSewa'] ?? '', 'rental_period': rentalData['waktuSewa'] ?? '',
'duration': rentalData['duration'] ?? '', 'duration': rentalData['duration'] ?? '',
'price_per_unit': 0, // This might not be available in rental data 'price_per_unit': 0, // This might not be available in rental data
'total_price': rentalData['totalPrice'] != null ? 'total_price':
int.tryParse(rentalData['totalPrice'].toString().replaceAll(RegExp(r'[^0-9]'), '')) ?? 0 : 0, rentalData['totalPrice'] != null
? int.tryParse(
rentalData['totalPrice'].toString().replaceAll(
RegExp(r'[^0-9]'),
'',
),
) ??
0
: 0,
'status': rentalData['status'] ?? 'MENUNGGU PEMBAYARAN', 'status': rentalData['status'] ?? 'MENUNGGU PEMBAYARAN',
'created_at': DateTime.now().toString(), 'created_at': DateTime.now().toString(),
'updated_at':
DateTime.now()
.toString(), // Explicitly set updated_at for countdown
'denda': 0, // Default value 'denda': 0, // Default value
'keterangan': '', // Default value 'keterangan': '', // Default value
'image_url': rentalData['imageUrl'], 'image_url': rentalData['imageUrl'],
@ -276,7 +320,7 @@ class PembayaranSewaController extends GetxController
checkSewaAsetTableStructure(); checkSewaAsetTableStructure();
loadTagihanSewaDetails().then((_) { loadTagihanSewaDetails().then((_) {
// Load existing payment proof images after tagihan_sewa details are loaded // Load existing payment proof images after tagihan_sewa details are loaded
loadExistingPaymentProofImages(); loadExistingPaymentProofImages(jenisPembayaran: 'tagihan awal');
}); });
loadSewaAsetDetails(); loadSewaAsetDetails();
loadBankAccounts(); // Load bank accounts data loadBankAccounts(); // Load bank accounts data
@ -286,7 +330,7 @@ class PembayaranSewaController extends GetxController
loadOrderDetails(); loadOrderDetails();
loadTagihanSewaDetails().then((_) { loadTagihanSewaDetails().then((_) {
// Load existing payment proof images after tagihan_sewa details are loaded // Load existing payment proof images after tagihan_sewa details are loaded
loadExistingPaymentProofImages(); loadExistingPaymentProofImages(jenisPembayaran: 'tagihan awal');
}); });
loadSewaAsetDetails(); loadSewaAsetDetails();
loadBankAccounts(); // Load bank accounts data loadBankAccounts(); // Load bank accounts data
@ -318,13 +362,19 @@ class PembayaranSewaController extends GetxController
'price_per_unit': 10000, 'price_per_unit': 10000,
'total_price': 50000, 'total_price': 50000,
'status': 'MENUNGGU PEMBAYARAN', 'status': 'MENUNGGU PEMBAYARAN',
'created_at': 'created_at': DateTime.now().toString(),
DateTime.now().toString(), // Use this for countdown calculation 'updated_at':
DateTime.now()
.toString(), // Explicitly set updated_at for countdown
'denda': 20000, // Dummy data for denda 'denda': 20000, // Dummy data for denda
'keterangan': 'keterangan':
'Terjadi kerusakan pada bagian kaki', // Dummy keterangan for denda 'Terjadi kerusakan pada bagian kaki', // Dummy keterangan for denda
}; };
debugPrint(
'DEBUG: Set updated_at in orderDetails: ${orderDetails.value['updated_at']}',
);
// Update the current step based on the status // Update the current step based on the status
updateCurrentStepBasedOnStatus(); updateCurrentStepBasedOnStatus();
@ -351,6 +401,11 @@ class PembayaranSewaController extends GetxController
'✅ Sewa aset details loaded: ${sewaAsetDetails.value['id']}', '✅ Sewa aset details loaded: ${sewaAsetDetails.value['id']}',
); );
// If this is a package order, load package details
if (isPaket.value && paketId.value.isNotEmpty) {
loadPaketDetails();
}
// Debug all fields in the sewaAsetDetails // Debug all fields in the sewaAsetDetails
debugPrint('📋 SEWA ASET DETAILS (COMPLETE DATA):'); debugPrint('📋 SEWA ASET DETAILS (COMPLETE DATA):');
data.forEach((key, value) { data.forEach((key, value) {
@ -382,18 +437,32 @@ class PembayaranSewaController extends GetxController
} }
val?['quantity'] = data['kuantitas'] ?? 1; val?['quantity'] = data['kuantitas'] ?? 1;
val?['denda'] = val?['denda'] =
data['denda'] ?? data['denda'] ?? 0; // Use data from API or default to 0
0; // Use data from API or default to 0 val?['keterangan'] = data['keterangan'] ?? '';
val?['keterangan'] = if (data['status'] != null &&
data['keterangan'] ?? data['status'].toString().isNotEmpty) {
''; // Use data from API or default to empty string
// Update status if it exists in the data
if (data['status'] != null && data['status'].toString().isNotEmpty) {
val?['status'] = data['status']; val?['status'] = data['status'];
debugPrint('📊 Order status from sewa_aset: ${data['status']}'); debugPrint('📊 Order status from sewa_aset: ${data['status']}');
} }
// Ensure updated_at is always set
if (data['updated_at'] != null) {
val?['updated_at'] = data['updated_at'];
debugPrint(
'📅 Using updated_at from database: ${data['updated_at']}',
);
} else if (data['created_at'] != null) {
val?['updated_at'] = data['created_at'];
debugPrint(
'📅 Using created_at as fallback for updated_at: ${data['created_at']}',
);
} else {
val?['updated_at'] = DateTime.now().toIso8601String();
debugPrint(
'📅 Using current timestamp as fallback for updated_at',
);
}
// Format rental period // Format rental period
if (data['waktu_mulai'] != null && if (data['waktu_mulai'] != null &&
data['waktu_selesai'] != null) { data['waktu_selesai'] != null) {
@ -406,7 +475,7 @@ class PembayaranSewaController extends GetxController
'✅ Successfully formatted rental period: ${val?['rental_period']}', '✅ Successfully formatted rental period: ${val?['rental_period']}',
); );
} catch (e) { } catch (e) {
debugPrint('❌ Error parsing date: $e'); debugPrint('❌ Error parsing date: ${e}');
} }
} else { } else {
debugPrint( debugPrint(
@ -530,7 +599,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Pesanan Dibatalkan', 'Pesanan Dibatalkan',
'Batas waktu pembayaran telah berakhir', 'Batas waktu pembayaran telah berakhir',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
duration: Duration(seconds: 5), duration: Duration(seconds: 5),
@ -547,6 +616,11 @@ class PembayaranSewaController extends GetxController
// Select payment type (tagihan_awal or denda) // Select payment type (tagihan_awal or denda)
void selectPaymentType(String type) { void selectPaymentType(String type) {
selectedPaymentType.value = type; selectedPaymentType.value = type;
if (type == 'tagihan_awal') {
loadExistingPaymentProofImages(jenisPembayaran: 'tagihan awal');
} else if (type == 'denda') {
loadExistingPaymentProofImages(jenisPembayaran: 'denda');
}
update(); update();
} }
@ -558,22 +632,21 @@ class PembayaranSewaController extends GetxController
source: ImageSource.camera, source: ImageSource.camera,
imageQuality: 80, imageQuality: 80,
); );
if (image != null) { if (image != null) {
// Add to the list of images instead of replacing if (selectedPaymentType.value == 'denda') {
paymentProofImages.add(File(image.path)); paymentProofImagesDenda.add(File(image.path));
} else {
// Check for changes paymentProofImagesTagihanAwal.add(File(image.path));
}
_checkForChanges(); _checkForChanges();
update(); update();
} }
} catch (e) { } catch (e) {
debugPrint('❌ Error taking photo: $e'); debugPrint('❌ Error taking photo: $e');
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal mengambil foto: ${e.toString()}', 'Gagal mengambil foto: \\${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -588,18 +661,21 @@ class PembayaranSewaController extends GetxController
source: ImageSource.gallery, source: ImageSource.gallery,
imageQuality: 80, imageQuality: 80,
); );
if (image != null) { if (image != null) {
// Add to the list of images instead of replacing if (selectedPaymentType.value == 'denda') {
paymentProofImages.add(File(image.path)); paymentProofImagesDenda.add(File(image.path));
} else {
paymentProofImagesTagihanAwal.add(File(image.path));
}
_checkForChanges();
update(); update();
} }
} catch (e) { } catch (e) {
debugPrint('❌ Error selecting photo from gallery: $e'); debugPrint('❌ Error selecting photo from gallery: $e');
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memilih foto dari galeri: ${e.toString()}', 'Gagal memilih foto dari galeri: \\${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -607,13 +683,25 @@ class PembayaranSewaController extends GetxController
} }
// Upload payment proof to Supabase storage and save to foto_pembayaran table // Upload payment proof to Supabase storage and save to foto_pembayaran table
Future<void> uploadPaymentProof() async { Future<void> uploadPaymentProof({required String jenisPembayaran}) async {
final paymentProofImages =
jenisPembayaran == 'tagihan awal'
? paymentProofImagesTagihanAwal
: paymentProofImagesDenda;
final imagesToDelete =
jenisPembayaran == 'tagihan awal'
? imagesToDeleteTagihanAwal
: imagesToDeleteDenda;
final hasUnsavedChanges =
jenisPembayaran == 'tagihan awal'
? hasUnsavedChangesTagihanAwal
: hasUnsavedChangesDenda;
// If there are no images and none marked for deletion, show error // If there are no images and none marked for deletion, show error
if (paymentProofImages.isEmpty && imagesToDelete.isEmpty) { if (paymentProofImages.isEmpty && imagesToDelete.isEmpty) {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Mohon unggah bukti pembayaran terlebih dahulu', 'Mohon unggah bukti pembayaran terlebih dahulu',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -625,7 +713,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Tidak ada perubahan yang perlu disimpan', 'Tidak ada perubahan yang perlu disimpan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
colorText: Colors.white, colorText: Colors.white,
); );
@ -644,7 +732,9 @@ class PembayaranSewaController extends GetxController
// First, delete any images marked for deletion // First, delete any images marked for deletion
if (imagesToDelete.isNotEmpty) { if (imagesToDelete.isNotEmpty) {
debugPrint('🗑️ Deleting ${imagesToDelete.length} images from database and storage'); debugPrint(
'🗑️ Deleting ${imagesToDelete.length} images from database and storage',
);
for (WebImageFile image in imagesToDelete) { for (WebImageFile image in imagesToDelete) {
// Delete the record from the foto_pembayaran table // Delete the record from the foto_pembayaran table
@ -673,14 +763,16 @@ class PembayaranSewaController extends GetxController
// The filename is the last part of the path after the last '/' // The filename is the last part of the path after the last '/'
final String fileName = path.substring(path.lastIndexOf('/') + 1); final String fileName = path.substring(path.lastIndexOf('/') + 1);
debugPrint('🗑️ Attempting to delete file from storage: $fileName'); debugPrint(
'🗑️ Attempting to delete file from storage: $fileName',
);
// Delete the file from storage // Delete the file from storage
await client.storage await client.storage.from('bukti.pembayaran').remove([fileName]);
.from('bukti.pembayaran')
.remove([fileName]);
debugPrint('🗑️ Successfully deleted file from storage: $fileName'); debugPrint(
'🗑️ Successfully deleted file from storage: $fileName',
);
} catch (e) { } catch (e) {
debugPrint('⚠️ Error deleting file from storage: $e'); debugPrint('⚠️ Error deleting file from storage: $e');
// Continue even if file deletion fails - we've at least deleted from the database // Continue even if file deletion fails - we've at least deleted from the database
@ -693,7 +785,9 @@ class PembayaranSewaController extends GetxController
} }
// Upload each new image to Supabase Storage and save to database // Upload each new image to Supabase Storage and save to database
debugPrint('🔄 Uploading new payment proof images to Supabase storage...'); debugPrint(
'🔄 Uploading new payment proof images to Supabase storage...',
);
List<String> uploadedUrls = []; List<String> uploadedUrls = [];
List<dynamic> newImagesToUpload = []; List<dynamic> newImagesToUpload = [];
@ -710,7 +804,9 @@ class PembayaranSewaController extends GetxController
} }
} }
debugPrint('🔄 Found ${existingImageUrls.length} existing images and ${newImagesToUpload.length} new images to upload'); debugPrint(
'🔄 Found ${existingImageUrls.length} existing images and ${newImagesToUpload.length} new images to upload',
);
// If there are new images to upload // If there are new images to upload
if (newImagesToUpload.isNotEmpty) { if (newImagesToUpload.isNotEmpty) {
@ -721,13 +817,16 @@ class PembayaranSewaController extends GetxController
// Upload each new image // Upload each new image
for (int i = 0; i < newImagesToUpload.length; i++) { for (int i = 0; i < newImagesToUpload.length; i++) {
final dynamic imageFile = newImagesToUpload[i]; final dynamic imageFile = newImagesToUpload[i];
final String fileName = '${DateTime.now().millisecondsSinceEpoch}_${orderId.value}_$i.jpg'; final String fileName =
'${DateTime.now().millisecondsSinceEpoch}_${orderId.value}_$i.jpg';
// Create a sub-progress tracker for this image // Create a sub-progress tracker for this image
final subProgressNotifier = StreamController<double>(); final subProgressNotifier = StreamController<double>();
subProgressNotifier.stream.listen((subProgress) { subProgressNotifier.stream.listen((subProgress) {
// Calculate overall progress // Calculate overall progress
progressNotifier.add(currentProgress + (subProgress * progressIncrement)); progressNotifier.add(
currentProgress + (subProgress * progressIncrement),
);
}); });
// Upload to Supabase Storage // Upload to Supabase Storage
@ -754,15 +853,20 @@ class PembayaranSewaController extends GetxController
// Save all new URLs to foto_pembayaran table // Save all new URLs to foto_pembayaran table
for (String imageUrl in uploadedUrls) { for (String imageUrl in uploadedUrls) {
await _saveToFotoPembayaranTable(imageUrl); await _saveToFotoPembayaranTable(imageUrl, jenisPembayaran);
} }
// Reload the existing images to get fresh data with new IDs // Reload the existing images to get fresh data with new IDs
await loadExistingPaymentProofImages(); await loadExistingPaymentProofImages(jenisPembayaran: jenisPembayaran);
// Update order status in orderDetails // Update order status in orderDetails
orderDetails.update((val) { orderDetails.update((val) {
val?['status'] = 'MEMERIKSA PEMBAYARAN'; if (jenisPembayaran == 'denda' &&
val?['status'] == 'PEMBAYARAN DENDA') {
val?['status'] = 'PERIKSA PEMBAYARAN DENDA';
} else {
val?['status'] = 'PERIKSA PEMBAYARAN';
}
}); });
// Also update the status in the sewa_aset table // Also update the status in the sewa_aset table
@ -771,17 +875,28 @@ class PembayaranSewaController extends GetxController
final dynamic sewaAsetId = tagihanSewa.value['sewa_aset_id']; final dynamic sewaAsetId = tagihanSewa.value['sewa_aset_id'];
if (sewaAsetId != null && sewaAsetId.toString().isNotEmpty) { if (sewaAsetId != null && sewaAsetId.toString().isNotEmpty) {
debugPrint('🔄 Updating status in sewa_aset table for ID: $sewaAsetId'); debugPrint(
'🔄 Updating status in sewa_aset table for ID: $sewaAsetId',
);
// Update the status in the sewa_aset table // Update the status in the sewa_aset table
final updateResult = await client final updateResult = await client
.from('sewa_aset') .from('sewa_aset')
.update({'status': 'PERIKSA PEMBAYARAN'}) .update({
'status':
(jenisPembayaran == 'denda' &&
orderDetails.value['status'] ==
'PERIKSA PEMBAYARAN DENDA')
? 'PERIKSA PEMBAYARAN DENDA'
: 'PERIKSA PEMBAYARAN',
})
.eq('id', sewaAsetId.toString()); .eq('id', sewaAsetId.toString());
debugPrint('✅ Status updated in sewa_aset table: $updateResult'); debugPrint('✅ Status updated in sewa_aset table: $updateResult');
} else { } else {
debugPrint('⚠️ Could not update sewa_aset status: No valid sewa_aset_id found'); debugPrint(
'⚠️ Could not update sewa_aset status: No valid sewa_aset_id found',
);
} }
} catch (e) { } catch (e) {
// Don't fail the entire operation if this update fails // Don't fail the entire operation if this update fails
@ -801,7 +916,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Bukti pembayaran berhasil diunggah', 'Bukti pembayaran berhasil diunggah',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -810,7 +925,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal mengunggah bukti pembayaran: ${e.toString()}', 'Gagal mengunggah bukti pembayaran: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -837,19 +952,19 @@ class PembayaranSewaController extends GetxController
newStatus = 'MENUNGGU PEMBAYARAN'; newStatus = 'MENUNGGU PEMBAYARAN';
break; break;
case 1: case 1:
newStatus = 'MEMERIKSA PEMBAYARAN'; newStatus = 'PERIKSA PEMBAYARAN';
break; break;
case 2: case 2:
newStatus = 'DITERIMA'; newStatus = 'DITERIMA';
break; break;
case 3: case 3:
newStatus = 'PENGEMBALIAN'; newStatus = 'DIKEMBALIKAN';
break; break;
case 4: case 4:
newStatus = 'PEMBAYARAN DENDA'; newStatus = 'PEMBAYARAN DENDA';
break; break;
case 5: case 5:
newStatus = 'MEMERIKSA PEMBAYARAN DENDA'; newStatus = 'PERIKSA PEMBAYARAN DENDA';
break; break;
case 6: case 6:
newStatus = 'SELESAI'; newStatus = 'SELESAI';
@ -872,27 +987,29 @@ class PembayaranSewaController extends GetxController
case 'MENUNGGU PEMBAYARAN': case 'MENUNGGU PEMBAYARAN':
currentStep.value = 0; currentStep.value = 0;
break; break;
case 'MEMERIKSA PEMBAYARAN': case 'PERIKSA PEMBAYARAN':
currentStep.value = 1; currentStep.value = 1;
break; break;
case 'DITERIMA': case 'DITERIMA':
currentStep.value = 2; currentStep.value = 2;
break; break;
case 'PENGEMBALIAN': case 'AKTIF':
currentStep.value = 3; currentStep.value = 3;
break; break;
case 'PEMBAYARAN DENDA': case 'DIKEMBALIKAN':
currentStep.value = 4; currentStep.value = 4;
break; break;
case 'MEMERIKSA PEMBAYARAN DENDA': case 'PEMBAYARAN DENDA':
currentStep.value = 5; currentStep.value = 5;
break; break;
case 'SELESAI': case 'PERIKSA PEMBAYARAN DENDA':
currentStep.value = 6; currentStep.value = 6;
break; break;
case 'SELESAI':
currentStep.value = 7;
break;
case 'DIBATALKAN': case 'DIBATALKAN':
// Special case for canceled orders currentStep.value = 8;
currentStep.value = 0;
break; break;
default: default:
currentStep.value = 0; currentStep.value = 0;
@ -908,7 +1025,7 @@ class PembayaranSewaController extends GetxController
void submitCashPayment() { void submitCashPayment() {
// Update order status // Update order status
orderDetails.update((val) { orderDetails.update((val) {
val?['status'] = 'MEMERIKSA PEMBAYARAN'; val?['status'] = 'PERIKSA PEMBAYARAN';
}); });
// Cancel countdown timer as payment has been submitted // Cancel countdown timer as payment has been submitted
@ -918,7 +1035,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Pembayaran tunai berhasil disubmit', 'Pembayaran tunai berhasil disubmit',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -950,7 +1067,9 @@ class PembayaranSewaController extends GetxController
debugPrint('Available fields in sewa_aset table:'); debugPrint('Available fields in sewa_aset table:');
record.forEach((key, value) { record.forEach((key, value) {
debugPrint(' $key: (${value != null ? value.runtimeType : 'null'})'); debugPrint(
' $key: (${value != null ? value.runtimeType : 'null'})',
);
}); });
// Specifically check for time fields // Specifically check for time fields
@ -987,12 +1106,16 @@ class PembayaranSewaController extends GetxController
final data = await asetProvider.getBankAccounts(); final data = await asetProvider.getBankAccounts();
if (data.isNotEmpty) { if (data.isNotEmpty) {
bankAccounts.assignAll(data); bankAccounts.assignAll(data);
debugPrint('✅ Bank accounts loaded: ${bankAccounts.length} accounts found'); debugPrint(
'✅ Bank accounts loaded: ${bankAccounts.length} accounts found',
);
// Debug the bank accounts data // Debug the bank accounts data
debugPrint('📋 BANK ACCOUNTS DETAILS:'); debugPrint('📋 BANK ACCOUNTS DETAILS:');
for (var account in bankAccounts) { for (var account in bankAccounts) {
debugPrint(' Bank: ${account['nama_bank']}, Account: ${account['nama_akun']}, Number: ${account['no_rekening']}'); debugPrint(
' Bank: ${account['nama_bank']}, Account: ${account['nama_akun']}, Number: ${account['no_rekening']}',
);
} }
} else { } else {
debugPrint('⚠️ No bank accounts found in akun_bank table'); debugPrint('⚠️ No bank accounts found in akun_bank table');
@ -1003,7 +1126,11 @@ class PembayaranSewaController extends GetxController
} }
// Helper method to upload image to Supabase storage // Helper method to upload image to Supabase storage
Future<String?> _uploadToSupabaseStorage(dynamic imageFile, String fileName, StreamController<double> progressNotifier) async { Future<String?> _uploadToSupabaseStorage(
dynamic imageFile,
String fileName,
StreamController<double> progressNotifier,
) async {
try { try {
debugPrint('🔄 Uploading image to Supabase storage: $fileName'); debugPrint('🔄 Uploading image to Supabase storage: $fileName');
@ -1031,7 +1158,9 @@ class PembayaranSewaController extends GetxController
); );
// Get public URL // Get public URL
final String publicUrl = client.storage.from('bukti.pembayaran').getPublicUrl(fileName); final String publicUrl = client.storage
.from('bukti.pembayaran')
.getPublicUrl(fileName);
debugPrint('✅ Upload successful: $publicUrl'); debugPrint('✅ Upload successful: $publicUrl');
progressNotifier.add(1.0); // Upload complete progressNotifier.add(1.0); // Upload complete
@ -1050,7 +1179,10 @@ class PembayaranSewaController extends GetxController
} }
// Helper method to save image URL to foto_pembayaran table // Helper method to save image URL to foto_pembayaran table
Future<void> _saveToFotoPembayaranTable(String imageUrl) async { Future<void> _saveToFotoPembayaranTable(
String imageUrl,
String jenisPembayaran,
) async {
try { try {
debugPrint('🔄 Saving image URL to foto_pembayaran table...'); debugPrint('🔄 Saving image URL to foto_pembayaran table...');
@ -1067,79 +1199,57 @@ class PembayaranSewaController extends GetxController
final Map<String, dynamic> data = { final Map<String, dynamic> data = {
'tagihan_sewa_id': tagihanSewaId, 'tagihan_sewa_id': tagihanSewaId,
'foto_pembayaran': imageUrl, 'foto_pembayaran': imageUrl,
'jenis_pembayaran': jenisPembayaran,
'created_at': DateTime.now().toIso8601String(), 'created_at': DateTime.now().toIso8601String(),
}; };
// Insert data into the foto_pembayaran table // Insert data into the foto_pembayaran table
final response = await client final response =
.from('foto_pembayaran') await client.from('foto_pembayaran').insert(data).select().single();
.insert(data)
.select()
.single();
debugPrint('✅ Image URL saved to foto_pembayaran table: ${response['id']}'); debugPrint(
'✅ Image URL saved to foto_pembayaran table: ${response['id']}',
);
} catch (e) { } catch (e) {
debugPrint('❌ Error in _saveToFotoPembayaranTable: $e'); debugPrint('❌ Error in _saveToFotoPembayaranTable: $e');
throw Exception('Failed to save image URL to database: $e'); throw Exception('Failed to save image URL to database: $e');
} }
} }
// Load existing payment proof images // Load existing payment proof images for a specific jenis_pembayaran
Future<void> loadExistingPaymentProofImages() async { Future<void> loadExistingPaymentProofImages({
required String jenisPembayaran,
}) async {
try { try {
debugPrint('🔄 Loading existing payment proof images for tagihan_sewa_id: ${tagihanSewa.value['id']}'); debugPrint(
'🔄 Loading existing payment proof images for tagihan_sewa_id: \\${tagihanSewa.value['id']} dan jenis_pembayaran: $jenisPembayaran',
// Check if we have a valid tagihan_sewa_id );
final dynamic tagihanSewaId = tagihanSewa.value['id']; final dynamic tagihanSewaId = tagihanSewa.value['id'];
if (tagihanSewaId == null || tagihanSewaId.toString().isEmpty) { if (tagihanSewaId == null || tagihanSewaId.toString().isEmpty) {
debugPrint('⚠️ No valid tagihan_sewa_id found, skipping image load'); debugPrint('⚠️ No valid tagihan_sewa_id found, skipping image load');
return; return;
} }
// First, make a test query to see the structure of the response
final testResponse = await client
.from('foto_pembayaran')
.select()
.limit(1);
// Log the test response structure
if (testResponse.isNotEmpty) {
debugPrint('💾 DEBUG: Test database response: ${testResponse[0]}');
testResponse[0].forEach((key, value) {
debugPrint('💾 DEBUG: Field $key = $value (${value?.runtimeType})');
});
}
// Now make the actual query for this tagihan_sewa_id
final List<dynamic> response = await client final List<dynamic> response = await client
.from('foto_pembayaran') .from('foto_pembayaran')
.select() .select()
.eq('tagihan_sewa_id', tagihanSewaId) .eq('tagihan_sewa_id', tagihanSewaId)
.eq('jenis_pembayaran', jenisPembayaran)
.order('created_at', ascending: false); .order('created_at', ascending: false);
debugPrint(
debugPrint('🔄 Found ${response.length} existing payment proof images'); '🔄 Found \\${response.length} existing payment proof images for $jenisPembayaran',
);
// Clear existing tracking lists final targetList =
paymentProofImages.clear(); jenisPembayaran == 'tagihan awal'
originalImages.clear(); ? paymentProofImagesTagihanAwal
imagesToDelete.clear(); : paymentProofImagesDenda;
hasUnsavedChanges.value = false; targetList.clear();
// Process each image in the response
for (final item in response) { for (final item in response) {
// Extract the image URL
final String imageUrl = item['foto_pembayaran']; final String imageUrl = item['foto_pembayaran'];
// Extract the ID - debug the item structure
debugPrint('💾 Image data: $item');
// Get the ID field - in Supabase, this is a UUID string
String imageId = ''; String imageId = '';
try { try {
if (item.containsKey('id')) { if (item.containsKey('id')) {
final dynamic rawId = item['id']; final dynamic rawId = item['id'];
if (rawId != null) { if (rawId != null) {
// Store ID as string since it's a UUID
imageId = rawId.toString(); imageId = rawId.toString();
} }
debugPrint('🔄 Image ID: $imageId'); debugPrint('🔄 Image ID: $imageId');
@ -1147,21 +1257,12 @@ class PembayaranSewaController extends GetxController
} catch (e) { } catch (e) {
debugPrint('❌ Error getting image ID: $e'); debugPrint('❌ Error getting image ID: $e');
} }
// Create the WebImageFile object
final webImageFile = WebImageFile(imageUrl); final webImageFile = WebImageFile(imageUrl);
webImageFile.id = imageId; webImageFile.id = imageId;
targetList.add(webImageFile);
// Add to tracking lists
paymentProofImages.add(webImageFile);
originalImages.add(webImageFile);
debugPrint('✅ Added image: $imageUrl with ID: $imageId'); debugPrint('✅ Added image: $imageUrl with ID: $imageId');
} }
// Update the UI
update(); update();
} catch (e) { } catch (e) {
debugPrint('❌ Error loading payment proof images: $e'); debugPrint('❌ Error loading payment proof images: $e');
} }
@ -1174,7 +1275,9 @@ class PembayaranSewaController extends GetxController
try { try {
// Reload all data // Reload all data
await Future.delayed(const Duration(milliseconds: 500)); // Small delay for better UX await Future.delayed(
const Duration(milliseconds: 500),
); // Small delay for better UX
loadOrderDetails(); loadOrderDetails();
loadTagihanSewaDetails(); loadTagihanSewaDetails();
loadSewaAsetDetails(); loadSewaAsetDetails();
@ -1185,7 +1288,20 @@ class PembayaranSewaController extends GetxController
updateCurrentStepBasedOnStatus(); updateCurrentStepBasedOnStatus();
// Restart countdown timer if needed // Restart countdown timer if needed
if (orderDetails.value['status'] == 'MENUNGGU PEMBAYARAN') { if ((orderDetails.value['status'] ?? '').toString().toUpperCase() ==
'MENUNGGU PEMBAYARAN') {
debugPrint('Status is MENUNGGU PEMBAYARAN, restarting countdown timer');
// Ensure updated_at is set to current time if refreshing with MENUNGGU PEMBAYARAN status
if (orderDetails.value['updated_at'] == null) {
orderDetails.update((val) {
val?['updated_at'] = DateTime.now().toIso8601String();
});
debugPrint(
'Set updated_at to current time: ${orderDetails.value['updated_at']}',
);
}
_countdownTimer?.cancel(); _countdownTimer?.cancel();
startCountdownTimer(); startCountdownTimer();
} }
@ -1199,4 +1315,56 @@ class PembayaranSewaController extends GetxController
return Future.value(); return Future.value();
} }
// Load package details and items
Future<void> loadPaketDetails() async {
if (!isPaket.value || paketId.value.isEmpty) return;
try {
debugPrint('🔄 Loading package details for ID: ${paketId.value}');
// Get package details
final paketResponse =
await client
.from('paket')
.select('*')
.eq('id', paketId.value)
.maybeSingle();
if (paketResponse != null) {
paketDetails.value = paketResponse;
debugPrint('✅ Package details loaded: ${paketDetails.value['nama']}');
}
// Load package items
debugPrint('🔄 Loading package items for package ID: ${paketId.value}');
final itemsResponse = await client
.from('paket_item')
.select('*, aset(*)')
.eq('paket_id', paketId.value);
if (itemsResponse != null &&
itemsResponse is List &&
itemsResponse.isNotEmpty) {
paketItems.value = List<Map<String, dynamic>>.from(itemsResponse);
isPaketItemsLoaded.value = true;
debugPrint('✅ Loaded ${paketItems.length} package items');
} else {
paketItems.clear();
debugPrint('⚠️ No package items found');
}
} catch (e) {
debugPrint('❌ Error loading package details: $e');
}
}
// Check if the sewa_aset table has the necessary columns
// Handle back button press - navigate to warga sewa page
void onBackPressed() {
debugPrint(
'🔙 Back button pressed in PembayaranSewaView - navigating to WargaSewa',
);
navigationService.toWargaSewa();
}
} }

View File

@ -88,6 +88,19 @@ class SewaAsetController extends GetxController
void onReady() { void onReady() {
super.onReady(); super.onReady();
debugPrint('🚀 SewaAsetController: onReady called'); 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 @override
@ -155,7 +168,7 @@ class SewaAsetController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Terjadi kesalahan saat memuat data aset', 'Terjadi kesalahan saat memuat data aset',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -266,7 +279,7 @@ class SewaAsetController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
message, message,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -308,7 +321,7 @@ class SewaAsetController extends GetxController
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Pesanan berhasil dibuat', 'Pesanan berhasil dibuat',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -370,7 +383,7 @@ class SewaAsetController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Terjadi kesalahan saat memuat data paket', 'Terjadi kesalahan saat memuat data paket',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -416,7 +429,7 @@ class SewaAsetController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memuat data paket. Silakan coba lagi nanti.', 'Gagal memuat data paket. Silakan coba lagi nanti.',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -457,7 +470,7 @@ class SewaAsetController extends GetxController
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Pesanan paket berhasil dibuat', 'Pesanan paket berhasil dibuat',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );

View File

@ -1,12 +1,20 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart';
import '../../../data/providers/auth_provider.dart'; import '../../../data/providers/auth_provider.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../../../services/navigation_service.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 { class WargaDashboardController extends GetxController {
// Dependency injection // Dependency injection
final AuthProvider _authProvider = Get.find<AuthProvider>(); final AuthProvider _authProvider = Get.find<AuthProvider>();
final NavigationService navigationService = Get.find<NavigationService>(); final NavigationService navigationService = Get.find<NavigationService>();
final AsetProvider _asetProvider = Get.find<AsetProvider>();
// User data // User data
final userName = 'Pengguna Warga'.obs; final userName = 'Pengguna Warga'.obs;
@ -16,6 +24,10 @@ class WargaDashboardController extends GetxController {
final userNik = ''.obs; final userNik = ''.obs;
final userPhone = ''.obs; final userPhone = ''.obs;
final userAddress = ''.obs; final userAddress = ''.obs;
final userTanggalLahir = ''.obs;
final userRtRw = ''.obs;
final userKelurahanDesa = ''.obs;
final userKecamatan = ''.obs;
// Navigation state is now managed by NavigationService // Navigation state is now managed by NavigationService
@ -28,24 +40,57 @@ class WargaDashboardController extends GetxController {
// Active penalties // Active penalties
final activePenalties = <Map<String, dynamic>>[].obs; final activePenalties = <Map<String, dynamic>>[].obs;
// Summary counts
final diterimaCount = 0.obs;
final tagihanAktifCount = 0.obs;
final dendaAktifCount = 0.obs;
@override @override
void onInit() { void onInit() async {
super.onInit(); super.onInit();
// Set navigation index to Home (0) // Set navigation index to Home (0)
navigationService.setNavIndex(0); navigationService.setNavIndex(0);
// Load user data // Check if navigation is coming from login
_loadUserData(); final args = Get.arguments;
final bool isFromLogin = args != null && args['from_login'] == true;
// Load sample data if (isFromLogin) {
_loadSampleData(); print('onInit: Navigation from login detected, prioritizing data fetch');
}
// Load dummy data for bills and penalties // Verifikasi bahwa pengguna sudah login sebelum melakukan fetch data
loadDummyData(); if (_authProvider.currentUser != null) {
// Prioritize loading user profile data first
await fetchProfileFromWargaDesa();
// Load unpaid rentals // If the profile data was not loaded successfully, try again after a short delay
loadUnpaidRentals(); 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 { Future<void> _loadUserData() async {
@ -75,12 +120,24 @@ class WargaDashboardController extends GetxController {
userNik.value = await _authProvider.getUserNIK() ?? ''; userNik.value = await _authProvider.getUserNIK() ?? '';
userPhone.value = await _authProvider.getUserPhone() ?? ''; userPhone.value = await _authProvider.getUserPhone() ?? '';
userAddress.value = await _authProvider.getUserAddress() ?? ''; 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) { } catch (e) {
print('Error loading user data: $e'); print('Error loading user data: $e');
} }
} }
void _loadSampleData() { Future<void> _loadSampleData() async {
// Clear any existing data // Clear any existing data
activeRentals.clear(); activeRentals.clear();
@ -111,10 +168,37 @@ class WargaDashboardController extends GetxController {
navigationService.toSewaAset(); navigationService.toSewaAset();
} }
void refreshData() { Future<void> refreshData() async {
// Refresh data from repository print('refreshData: Refreshing dashboard data');
_loadSampleData(); try {
loadDummyData(); // 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) { void onNavItemTapped(int index) {
@ -129,18 +213,25 @@ class WargaDashboardController extends GetxController {
// Already on Home tab // Already on Home tab
break; break;
case 1: case 1:
// Navigate to Sewa page // Navigate to Sewa page, tab Aktif
navigationService.toWargaSewa(); toWargaSewaTabAktif();
break; break;
} }
} }
void logout() async { void toWargaSewaTabAktif() {
await _authProvider.signOut(); // Navigasi ke halaman warga sewa dan tab Aktif (index 3)
navigationService.toLogin(); 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 // Dummy active bills
activeBills.clear(); activeBills.clear();
activeBills.add({ activeBills.add({
@ -177,4 +268,555 @@ class WargaDashboardController extends GetxController {
print('Error loading unpaid rentals: $e'); 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);
} }

View File

@ -25,6 +25,8 @@ class WargaSewaController extends GetxController
final acceptedRentals = <Map<String, dynamic>>[].obs; final acceptedRentals = <Map<String, dynamic>>[].obs;
final completedRentals = <Map<String, dynamic>>[].obs; final completedRentals = <Map<String, dynamic>>[].obs;
final cancelledRentals = <Map<String, dynamic>>[].obs; final cancelledRentals = <Map<String, dynamic>>[].obs;
final returnedRentals = <Map<String, dynamic>>[].obs;
final activeRentals = <Map<String, dynamic>>[].obs;
// Loading states // Loading states
final isLoading = false.obs; final isLoading = false.obs;
@ -32,26 +34,26 @@ class WargaSewaController extends GetxController
final isLoadingAccepted = false.obs; final isLoadingAccepted = false.obs;
final isLoadingCompleted = false.obs; final isLoadingCompleted = false.obs;
final isLoadingCancelled = false.obs; final isLoadingCancelled = false.obs;
final isLoadingReturned = false.obs;
final isLoadingActive = false.obs;
bool _tabSetFromArgument = false;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
// Ensure tab index is set to Sewa (1) // Initialize tab controller with 7 tabs
navigationService.setNavIndex(1); tabController = TabController(length: 7, vsync: this);
// Initialize tab controller with 6 tabs
tabController = TabController(length: 6, vsync: this);
// Set initial tab and ensure tab view is updated
tabController.index = 0;
// Load real rental data for all tabs // Load real rental data for all tabs
loadRentalsData(); loadRentalsData();
loadPendingRentals(); loadPendingRentals();
loadAcceptedRentals(); loadAcceptedRentals();
loadActiveRentals();
loadCompletedRentals(); loadCompletedRentals();
loadCancelledRentals(); loadCancelledRentals();
loadReturnedRentals();
// Listen to tab changes to update state if needed // Listen to tab changes to update state if needed
tabController.addListener(() { tabController.addListener(() {
@ -77,7 +79,9 @@ class WargaSewaController extends GetxController
} }
break; break;
case 3: // Aktif case 3: // Aktif
// Add Aktif tab logic when needed if (activeRentals.isEmpty && !isLoadingActive.value) {
loadActiveRentals();
}
break; break;
case 4: // Selesai case 4: // Selesai
if (completedRentals.isEmpty && !isLoadingCompleted.value) { if (completedRentals.isEmpty && !isLoadingCompleted.value) {
@ -89,6 +93,11 @@ class WargaSewaController extends GetxController
loadCancelledRentals(); loadCancelledRentals();
} }
break; break;
case 6: // Dikembalikan
if (returnedRentals.isEmpty && !isLoadingReturned.value) {
loadReturnedRentals();
}
break;
} }
}); });
} }
@ -96,9 +105,26 @@ class WargaSewaController extends GetxController
@override @override
void onReady() { void onReady() {
super.onReady(); super.onReady();
// Ensure nav index is set to Sewa (1) when the controller is ready // Jalankan update nav index dan tab index setelah build selesai
// This helps maintain correct state during hot reload Future.delayed(Duration.zero, () {
navigationService.setNavIndex(1); 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 @override
@ -107,30 +133,27 @@ class WargaSewaController extends GetxController
super.onClose(); super.onClose();
} }
// Load real data from sewa_aset table // Helper method to process rental data
Future<void> loadRentalsData() async { Future<Map<String, dynamic>> _processRentalData(
try { Map<String, dynamic> sewaAset,
isLoading.value = true; ) async {
// 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'
]);
debugPrint('Fetched ${sewaAsetList.length} sewa_aset records');
// Process each sewa_aset record
for (var sewaAset in sewaAsetList) {
// Get asset details if aset_id is available // Get asset details if aset_id is available
String assetName = 'Aset'; String assetName = 'Aset';
String? imageUrl; String? imageUrl;
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam'; String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
if (sewaAset['aset_id'] != null) { // 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']); final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
if (asetData != null) { if (asetData != null) {
assetName = asetData.nama; assetName = asetData.nama;
@ -175,7 +198,8 @@ class WargaSewaController extends GetxController
} }
// Full time format for waktuSewa // Full time format for waktuSewa
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - ' waktuSewa =
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}'; '${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
} }
@ -190,15 +214,15 @@ class WargaSewaController extends GetxController
totalPrice = formatter.format(sewaAset['total']); totalPrice = formatter.format(sewaAset['total']);
} }
// Add to rentals list // Return processed rental data
rentals.add({ return {
'id': sewaAset['id'] ?? '', 'id': sewaAset['id'] ?? '',
'name': assetName, 'name': assetName,
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg', 'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
'jumlahUnit': sewaAset['kuantitas'] ?? 0, 'jumlahUnit': sewaAset['kuantitas'] ?? 0,
'waktuSewa': waktuSewa, 'waktuSewa': waktuSewa,
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}', 'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
'status': sewaAset['status'] ?? 'MENUNGGU PEMBAYARAN', 'status': sewaAset['status'] ?? '',
'totalPrice': totalPrice, 'totalPrice': totalPrice,
'countdown': '00:59:59', // Default countdown 'countdown': '00:59:59', // Default countdown
'tanggalSewa': tanggalSewa, 'tanggalSewa': tanggalSewa,
@ -208,7 +232,51 @@ class WargaSewaController extends GetxController
'namaSatuanWaktu': namaSatuanWaktu, 'namaSatuanWaktu': namaSatuanWaktu,
'waktuMulai': sewaAset['waktu_mulai'], 'waktuMulai': sewaAset['waktu_mulai'],
'waktuSelesai': sewaAset['waktu_selesai'], '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',
]);
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) {
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']}',
);
}
rentals.add(processedData);
} }
debugPrint('Processed ${rentals.length} rental records'); debugPrint('Processed ${rentals.length} rental records');
@ -245,12 +313,54 @@ class WargaSewaController extends GetxController
} }
// Actions // Actions
void cancelRental(String id) { void cancelRental(String id) async {
Get.snackbar( final confirmed = await Get.dialog<bool>(
'Info', AlertDialog(
'Pembatalan berhasil', title: const Text('Konfirmasi Pembatalan'),
snackPosition: SnackPosition.BOTTOM, 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 // Navigate to payment page with the selected rental data
@ -258,11 +368,25 @@ class WargaSewaController extends GetxController
debugPrint('Navigating to payment page with rental ID: ${rental['id']}'); debugPrint('Navigating to payment page with rental ID: ${rental['id']}');
// Navigate to payment page with rental data // 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( Get.toNamed(
Routes.PEMBAYARAN_SEWA, Routes.PEMBAYARAN_SEWA,
arguments: { arguments: {
'orderId': rental['id'], 'orderId': rental['id'],
'rentalData': rental, 'rentalData': rental,
'initialTab': 2, // Index 2 corresponds to the payment tab
'isPaket': rental['isPaket'] ?? false,
'paketId': rental['paketId'],
}, },
); );
} }
@ -271,7 +395,7 @@ class WargaSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Navigasi ke halaman pembayaran', 'Navigasi ke halaman pembayaran',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -290,92 +414,14 @@ class WargaSewaController extends GetxController
// Process each sewa_aset record // Process each sewa_aset record
for (var sewaAset in sewaAsetList) { for (var sewaAset in sewaAsetList) {
// Get asset details if aset_id is available final processedData = await _processRentalData(sewaAset);
String assetName = 'Aset'; processedData['status'] = sewaAset['status'] ?? 'SELESAI';
String? imageUrl; completedRentals.add(processedData);
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 debugPrint(
DateTime? waktuMulai; 'Processed ${completedRentals.length} completed rental records',
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'],
});
}
debugPrint('Processed ${completedRentals.length} completed rental records');
} catch (e) { } catch (e) {
debugPrint('Error loading completed rentals data: $e'); debugPrint('Error loading completed rentals data: $e');
} finally { } finally {
@ -392,99 +438,24 @@ class WargaSewaController extends GetxController
cancelledRentals.clear(); cancelledRentals.clear();
// Get sewa_aset data with status "DIBATALKAN" // 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'); debugPrint('Fetched ${sewaAsetList.length} cancelled sewa_aset records');
// Process each sewa_aset record // Process each sewa_aset record
for (var sewaAset in sewaAsetList) { for (var sewaAset in sewaAsetList) {
// Get asset details if aset_id is available final processedData = await _processRentalData(sewaAset);
String assetName = 'Aset'; processedData['status'] = sewaAset['status'] ?? 'DIBATALKAN';
String? imageUrl; processedData['alasanPembatalan'] =
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam'; sewaAset['alasan_pembatalan'] ?? '-';
cancelledRentals.add(processedData);
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 debugPrint(
DateTime? waktuMulai; 'Processed ${cancelledRentals.length} cancelled rental records',
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'] ?? '-',
});
}
debugPrint('Processed ${cancelledRentals.length} cancelled rental records');
} catch (e) { } catch (e) {
debugPrint('Error loading cancelled rentals data: $e'); debugPrint('Error loading cancelled rentals data: $e');
} finally { } finally {
@ -492,6 +463,64 @@ class WargaSewaController extends GetxController
} }
} }
// 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) // Load data for the Pending tab (status: PERIKSA PEMBAYARAN)
Future<void> loadPendingRentals() async { Future<void> loadPendingRentals() async {
try { try {
@ -500,96 +529,19 @@ class WargaSewaController extends GetxController
// Clear existing data // Clear existing data
pendingRentals.clear(); pendingRentals.clear();
// Get sewa_aset data with status "PERIKSA PEMBAYARAN" // Get sewa_aset data with status 'PERIKSA PEMBAYARAN' dan 'PERIKSA PEMBAYARAN DENDA'
final sewaAsetList = await authProvider.getSewaAsetByStatus(['PERIKSA PEMBAYARAN']); final sewaAsetList = await authProvider.getSewaAsetByStatus([
'PERIKSA PEMBAYARAN',
'PERIKSA PEMBAYARAN DENDA',
]);
debugPrint('Fetched ${sewaAsetList.length} pending sewa_aset records'); debugPrint('Fetched ${sewaAsetList.length} pending sewa_aset records');
// Process each sewa_aset record // Process each sewa_aset record
for (var sewaAset in sewaAsetList) { for (var sewaAset in sewaAsetList) {
// Get asset details if aset_id is available final processedData = await _processRentalData(sewaAset);
String assetName = 'Aset'; processedData['status'] = sewaAset['status'] ?? 'PERIKSA PEMBAYARAN';
String? imageUrl; pendingRentals.add(processedData);
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'],
});
} }
debugPrint('Processed ${pendingRentals.length} pending rental records'); debugPrint('Processed ${pendingRentals.length} pending rental records');
@ -615,89 +567,9 @@ class WargaSewaController extends GetxController
// Process each sewa_aset record // Process each sewa_aset record
for (var sewaAset in sewaAsetList) { for (var sewaAset in sewaAsetList) {
// Get asset details if aset_id is available final processedData = await _processRentalData(sewaAset);
String assetName = 'Aset'; processedData['status'] = sewaAset['status'] ?? 'DITERIMA';
String? imageUrl; acceptedRentals.add(processedData);
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'],
});
} }
debugPrint('Processed ${acceptedRentals.length} accepted rental records'); debugPrint('Processed ${acceptedRentals.length} accepted rental records');

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -318,7 +318,7 @@
Get.snackbar( Get.snackbar(
'Perhatian', 'Perhatian',
'Pilih jam mulai terlebih dahulu', 'Pilih jam mulai terlebih dahulu',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: AppColors.warning, backgroundColor: AppColors.warning,
colorText: Colors.white, colorText: Colors.white,
); );

File diff suppressed because it is too large Load Diff

View File

@ -68,7 +68,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
child: TextField( child: TextField(
controller: controller.searchController, controller: controller.searchController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Cari aset...', hintText: 'Cari aset atau paket...',
hintStyle: TextStyle(color: Colors.grey[400]), hintStyle: TextStyle(color: Colors.grey[400]),
prefixIcon: Icon(Icons.search, color: Colors.grey[600]), prefixIcon: Icon(Icons.search, color: Colors.grey[600]),
border: InputBorder.none, border: InputBorder.none,
@ -117,6 +117,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
), ),
], ],
), ),
dividerColor: Colors.transparent,
labelColor: Colors.white, labelColor: Colors.white,
unselectedLabelColor: const Color( unselectedLabelColor: const Color(
0xFF718093, 0xFF718093,
@ -363,9 +364,13 @@ class SewaAsetView extends GetView<SewaAsetController> {
); );
} }
return Padding( return RefreshIndicator(
onRefresh: controller.loadPakets,
color: const Color(0xFF3A6EA5), // Primary blue
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: GridView.builder( child: GridView.builder(
padding: const EdgeInsets.only(top: 16.0, bottom: 16.0),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, crossAxisCount: 2,
childAspectRatio: 0.50, // Make cards taller to avoid overflow childAspectRatio: 0.50, // Make cards taller to avoid overflow
@ -373,8 +378,6 @@ class SewaAsetView extends GetView<SewaAsetController> {
mainAxisSpacing: 16, mainAxisSpacing: 16,
), ),
itemCount: controller.filteredPakets.length, itemCount: controller.filteredPakets.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final paket = controller.filteredPakets[index]; final paket = controller.filteredPakets[index];
final List<dynamic> satuanWaktuSewa = final List<dynamic> satuanWaktuSewa =
@ -392,7 +395,9 @@ class SewaAsetView extends GetView<SewaAsetController> {
String imageUrl = paket['gambar_url'] ?? ''; String imageUrl = paket['gambar_url'] ?? '';
return GestureDetector( return GestureDetector(
onTap: () => _showPaketDetailModal(paket), onTap: () {
// No action when tapping on the card
},
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.surface, color: AppColors.surface,
@ -496,7 +501,8 @@ class SewaAsetView extends GetView<SewaAsetController> {
// Pastikan data yang ditampilkan valid // Pastikan data yang ditampilkan valid
final harga = sws['harga'] ?? 0; final harga = sws['harga'] ?? 0;
final namaSatuan = final namaSatuan =
sws['nama_satuan_waktu'] ?? 'Satuan'; sws['nama_satuan_waktu'] ??
'Satuan';
return Container( return Container(
margin: const EdgeInsets.only( margin: const EdgeInsets.only(
bottom: 4, bottom: 4,
@ -535,7 +541,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
], ],
), ),
); );
}).toList(), }),
], ],
), ),
) )
@ -548,7 +554,9 @@ class SewaAsetView extends GetView<SewaAsetController> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[100], color: Colors.grey[100],
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.grey[300]!), border: Border.all(
color: Colors.grey[300]!,
),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -571,7 +579,21 @@ class SewaAsetView extends GetView<SewaAsetController> {
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: () => _showPaketDetailModal(paket), 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( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary, backgroundColor: AppColors.primary,
foregroundColor: Colors.white, foregroundColor: Colors.white,
@ -581,7 +603,10 @@ class SewaAsetView extends GetView<SewaAsetController> {
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 6, vertical: 6,
), ),
minimumSize: const Size(double.infinity, 30), minimumSize: const Size(
double.infinity,
30,
),
tapTargetSize: tapTargetSize:
MaterialTapTargetSize.shrinkWrap, MaterialTapTargetSize.shrinkWrap,
), ),
@ -604,6 +629,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
); );
}, },
), ),
),
); );
}); });
} }
@ -893,35 +919,37 @@ class SewaAsetView extends GetView<SewaAsetController> {
// Order button // Order button
Padding( Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 16.0, bottom: 24.0),
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
height: 50, height: 50,
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
if (satuanWaktuSewa.isEmpty) { // Close the modal
Get.snackbar( Get.back();
'Tidak Dapat Memesan', // Navigate to order_sewa_paket page with package data
'Pilihan harga belum tersedia untuk paket ini', Get.toNamed(
snackPosition: SnackPosition.BOTTOM, Routes.ORDER_SEWA_PAKET,
backgroundColor: Colors.red[100], arguments: {
colorText: Colors.red[800], 'paket': paket,
'satuanWaktuSewa': satuanWaktuSewa,
},
); );
return;
}
_showOrderPaketForm(paket, satuanWaktuSewa);
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: AppColors.primary, backgroundColor: AppColors.primary,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
elevation: 2,
), ),
child: const Text( child: const Text(
'Pesan Paket Ini', 'Pesan Sekarang',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 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); final RxInt duration = RxInt(selectedSWS.value?['durasi_min'] ?? 1);
// Calculate total price // Calculate total price
final calculateTotal = () { calculateTotal() {
if (selectedSWS.value == null) return 0; if (selectedSWS.value == null) return 0;
return (selectedSWS.value!['harga'] ?? 0) * duration.value; return (selectedSWS.value!['harga'] ?? 0) * duration.value;
}; }
final RxInt totalPrice = RxInt(calculateTotal()); final RxInt totalPrice = RxInt(calculateTotal());
// Update total when duration or pricing option changes // Update total when duration or pricing option changes
@ -1231,7 +1263,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
], ],
), ),
Text( Text(
'Minimum ${minDuration} ${namaSatuanWaktu.toLowerCase()}', 'Minimum $minDuration ${namaSatuanWaktu.toLowerCase()}',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey[600], color: Colors.grey[600],
@ -1285,20 +1317,12 @@ class SewaAsetView extends GetView<SewaAsetController> {
onPressed: () { onPressed: () {
Get.back(); // Close the form Get.back(); // Close the form
// Navigate to order_sewa_paket page // Order the package
// Get the navigation service from the controller controller.placeOrderPaket(
final navigationService = controller.navigationService; paketId: paket['id'],
satuanWaktuSewaId: selectedSWS.value?['id'] ?? '',
// Store the selected parameters in a controller or pass as arguments durasi: duration.value,
Get.toNamed( totalHarga: totalPrice.value,
Routes.ORDER_SEWA_PAKET,
arguments: {
'paketId': paket['id'],
'satuanWaktuSewaId': selectedSWS.value?['id'] ?? '',
'durasi': duration.value,
'totalHarga': totalPrice.value,
'paketData': paket,
},
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -1500,7 +1524,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'ID aset tidak valid', 'ID aset tidak valid',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -1730,7 +1754,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'ID aset tidak valid', 'ID aset tidak valid',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -1777,15 +1801,22 @@ class SewaAsetView extends GetView<SewaAsetController> {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'ID aset tidak valid', 'ID aset tidak valid',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
return; return;
} }
// Use the static navigation method to ensure consistent behavior // Navigate to order page with asset ID and isAset flag
OrderSewaAsetController.navigateToOrderPage(aset.id); 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 // Helper to format numbers for display

Some files were not shown because too many files have changed in this diff Show More