fitur petugas

This commit is contained in:
Andreas Malvino
2025-06-22 09:25:58 +07:00
parent c4dd4fdfa2
commit 8284c93aa5
48 changed files with 8688 additions and 3436 deletions

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: json['updated_at'] != null
updatedAt: ? DateTime.parse(json['updated_at'])
json['updated_at'] != null : null,
? DateTime.parse(json['updated_at'])
: 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,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer' as developer;
class PaketModel { class PaketModel {
final String id; final String id;
@ -6,12 +7,13 @@ class PaketModel {
final String deskripsi; final String deskripsi;
final double harga; final double harga;
final int kuantitas; final int kuantitas;
final List<String> foto; final String status;
final List<Map<String, dynamic>> satuanWaktuSewa; List<String> foto;
List<Map<String, dynamic>> satuanWaktuSewa;
final DateTime createdAt; final DateTime createdAt;
final DateTime updatedAt; final DateTime updatedAt;
final String? foto_paket; // Main photo URL String? foto_paket; // Main photo URL
final List<String>? images; // List of photo URLs List<String>? images; // List of photo URLs
PaketModel({ PaketModel({
required this.id, required this.id,
@ -19,13 +21,47 @@ class PaketModel {
required this.deskripsi, required this.deskripsi,
required this.harga, required this.harga,
required this.kuantitas, required this.kuantitas,
required this.foto, this.status = 'aktif',
required this.satuanWaktuSewa, required List<String> foto,
required List<Map<String, dynamic>> satuanWaktuSewa,
this.foto_paket, this.foto_paket,
this.images, List<String>? images,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
}); }) : foto = List.from(foto),
satuanWaktuSewa = List.from(satuanWaktuSewa),
images = images != null ? List.from(images) : [];
// Add copyWith method for immutability patterns
PaketModel copyWith({
String? id,
String? nama,
String? deskripsi,
double? harga,
int? kuantitas,
String? status,
List<String>? foto,
List<Map<String, dynamic>>? satuanWaktuSewa,
String? foto_paket,
List<String>? images,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return PaketModel(
id: 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 // Alias for fromJson to maintain compatibility
factory PaketModel.fromMap(Map<String, dynamic> json) => PaketModel.fromJson(json); factory PaketModel.fromMap(Map<String, dynamic> json) => PaketModel.fromJson(json);
@ -63,10 +99,15 @@ class PaketModel {
} }
} }
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( return PaketModel(
id: json['id']?.toString() ?? '', id: json['id']?.toString() ?? '',
nama: json['nama']?.toString() ?? '', nama: json['nama']?.toString() ?? '',
deskripsi: json['deskripsi']?.toString() ?? '', deskripsi: json['deskripsi']?.toString() ?? '',
status: status,
harga: (json['harga'] is num) ? (json['harga'] as num).toDouble() : 0.0, harga: (json['harga'] is num) ? (json['harga'] as num).toDouble() : 0.0,
kuantitas: (json['kuantitas'] is num) ? (json['kuantitas'] as num).toInt() : 1, kuantitas: (json['kuantitas'] is num) ? (json['kuantitas'] as num).toInt() : 1,
foto: fotoList, foto: fotoList,
@ -97,35 +138,7 @@ class PaketModel {
'updated_at': updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
}; };
// Create a copy of the model with some fields updated
PaketModel copyWith({
String? id,
String? nama,
String? deskripsi,
double? harga,
int? kuantitas,
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,
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,
);
}
// Get the first photo URL or a placeholder // Get the first photo URL or a placeholder
String get firstPhotoUrl => foto.isNotEmpty ? foto.first : ''; String get firstPhotoUrl => foto.isNotEmpty ? foto.first : '';

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

View File

@ -1,3 +1,5 @@
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:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
@ -6,6 +8,8 @@ import '../models/foto_aset_model.dart';
import '../models/satuan_waktu_model.dart'; import '../models/satuan_waktu_model.dart';
import '../models/satuan_waktu_sewa_model.dart'; import '../models/satuan_waktu_sewa_model.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../models/paket_model.dart';
import '../providers/auth_provider.dart';
class AsetProvider extends GetxService { class AsetProvider extends GetxService {
late final SupabaseClient client; late final SupabaseClient client;
@ -24,8 +28,16 @@ class AsetProvider extends GetxService {
.from('aset') .from('aset')
.select('*') .select('*')
.eq('kategori', 'sewa') .eq('kategori', 'sewa')
.eq('status', 'tersedia') // Hanya yang tersedia .ilike('status', 'tersedia') // Hanya yang tersedia
.order('nama', ascending: true); // Urutan berdasarkan nama .order('nama', ascending: true) // Urutan berdasarkan nama
.withConverter<List<Map<String, dynamic>>>(
(data) =>
data.map<Map<String, dynamic>>((item) {
// Ensure 'jenis' is set to 'Sewa' for sewa assets
item['jenis'] = 'Sewa';
return item;
}).toList(),
);
debugPrint('Fetched ${response.length} aset'); debugPrint('Fetched ${response.length} aset');
@ -56,8 +68,16 @@ class AsetProvider extends GetxService {
.from('aset') .from('aset')
.select('*') .select('*')
.eq('kategori', 'langganan') .eq('kategori', 'langganan')
.eq('status', 'tersedia') // Hanya yang tersedia .ilike('status', 'tersedia') // Hanya yang tersedia
.order('nama', ascending: true); // Urutan berdasarkan nama .order('nama', ascending: true) // Urutan berdasarkan nama
.withConverter<List<Map<String, dynamic>>>(
(data) =>
data.map<Map<String, dynamic>>((item) {
// Ensure 'jenis' is set to 'Langganan' for langganan assets
item['jenis'] = 'Langganan';
return item;
}).toList(),
);
debugPrint('Fetched ${response.length} langganan aset'); debugPrint('Fetched ${response.length} langganan aset');
@ -120,9 +140,26 @@ class AsetProvider extends GetxService {
Future<void> loadAssetPhotos(AsetModel aset) async { Future<void> loadAssetPhotos(AsetModel aset) async {
try { try {
final photos = await getAsetPhotos(aset.id); final photos = await getAsetPhotos(aset.id);
if (photos.isNotEmpty && if (photos.isNotEmpty) {
(aset.imageUrl == null || aset.imageUrl!.isEmpty)) { // Clear existing images
aset.imageUrl = photos.first.fotoAset; aset.imageUrls.clear();
// Add all photos to the imageUrls list
for (final photo in photos) {
if (photo.fotoAset != null && photo.fotoAset!.isNotEmpty) {
aset.addImageUrl(photo.fotoAset);
}
}
// Set the main image URL if it's not already set
if ((aset.imageUrl == null || aset.imageUrl!.isEmpty) &&
aset.imageUrls.isNotEmpty) {
aset.imageUrl = aset.imageUrls.first;
}
debugPrint(
'✅ Loaded ${aset.imageUrls.length} photos for asset ${aset.id}',
);
} }
} catch (e) { } catch (e) {
debugPrint('Error loading asset photos for ID ${aset.id}: $e'); debugPrint('Error loading asset photos for ID ${aset.id}: $e');
@ -172,6 +209,376 @@ class AsetProvider extends GetxService {
} }
} }
// Create a new asset
Future<Map<String, dynamic>?> createAset(
Map<String, dynamic> asetData,
) async {
try {
debugPrint('🔄 Creating new aset with data:');
asetData.forEach((key, value) {
debugPrint(' $key: $value');
});
final response =
await client.from('aset').insert(asetData).select().single();
debugPrint('✅ Aset created successfully with ID: ${response['id']}');
return response;
} catch (e) {
debugPrint('❌ Error creating aset: $e');
debugPrint('❌ Stack trace: ${StackTrace.current}');
return null;
}
}
// Update an existing asset
Future<bool> updateAset(String asetId, Map<String, dynamic> asetData) async {
try {
debugPrint('🔄 Updating aset with ID: $asetId');
asetData.forEach((key, value) {
debugPrint(' $key: $value');
});
final response = await client
.from('aset')
.update(asetData)
.eq('id', asetId);
debugPrint('✅ Aset updated successfully');
return true;
} catch (e) {
debugPrint('❌ Error updating aset: $e');
debugPrint('❌ Stack trace: ${StackTrace.current}');
return false;
}
}
/// Adds a photo URL to the foto_aset table for a specific asset
Future<bool> addFotoAset({
required String asetId,
required String fotoUrl,
}) async {
try {
debugPrint('💾 Attempting to save foto to database:');
debugPrint(' - asetId: $asetId');
debugPrint(' - fotoUrl: $fotoUrl');
final data = {
'id_aset': asetId,
'foto_aset': fotoUrl,
'created_at': DateTime.now().toIso8601String(),
};
debugPrint('📤 Inserting into foto_aset table...');
final response = await client.from('foto_aset').insert(data).select();
debugPrint('📝 Database insert response: $response');
if (response == null) {
debugPrint('❌ Failed to insert into foto_aset: Response is null');
return false;
}
if (response is List && response.isNotEmpty) {
debugPrint('✅ Successfully added foto for aset ID: $asetId');
return true;
} else {
debugPrint('❌ Failed to add foto: Empty or invalid response');
return false;
}
} catch (e, stackTrace) {
debugPrint('❌ Error adding foto aset: $e');
debugPrint('Stack trace: $stackTrace');
return false;
}
}
/// Add satuan waktu sewa for an asset
Future<bool> addSatuanWaktuSewa({
required String asetId,
required String satuanWaktu,
required int harga,
required int maksimalWaktu,
}) async {
try {
// First, get the satuan_waktu_id from the satuan_waktu table
final response =
await client
.from('satuan_waktu')
.select('id')
.ilike('nama_satuan_waktu', satuanWaktu)
.maybeSingle();
if (response == null) {
debugPrint('❌ Satuan waktu "$satuanWaktu" not found in the database');
return false;
}
final satuanWaktuId = response['id'] as String;
final data = {
'aset_id': asetId,
'satuan_waktu_id': satuanWaktuId,
'harga': harga,
'maksimal_waktu': maksimalWaktu,
};
debugPrint('🔄 Adding satuan waktu sewa:');
data.forEach((key, value) {
debugPrint(' $key: $value');
});
await client.from('satuan_waktu_sewa').insert(data);
debugPrint('✅ Satuan waktu sewa added successfully');
return true;
} catch (e) {
debugPrint('❌ Error adding satuan waktu sewa: $e');
debugPrint('❌ Stack trace: ${StackTrace.current}');
return false;
}
}
// Delete all satuan waktu sewa for an asset
Future<bool> deleteSatuanWaktuSewaByAsetId(String asetId) async {
try {
await client
.from('satuan_waktu_sewa')
.delete()
.eq('aset_id', asetId); // Changed from 'id_aset' to 'aset_id'
debugPrint('✅ Deleted satuan waktu sewa for aset ID: $asetId');
return true;
} catch (e) {
debugPrint('❌ Error deleting satuan waktu sewa: $e');
return false;
}
}
/// Uploads a file to Supabase Storage root
/// Returns the public URL of the uploaded file, or null if upload fails
Future<String?> uploadFileToStorage(File file) async {
try {
if (!await file.exists()) {
debugPrint('❌ File does not exist: ${file.path}');
return null;
}
final fileName =
'${DateTime.now().millisecondsSinceEpoch}_${file.path.split(Platform.pathSeparator).last}';
debugPrint('🔄 Preparing to upload file: $fileName');
final uploadResponse = await client.storage
.from('foto.aset')
.upload(fileName, file, fileOptions: FileOptions(upsert: true));
debugPrint('📤 Upload response: $uploadResponse');
final publicUrl = client.storage.from('foto.aset').getPublicUrl(fileName);
debugPrint('✅ File uploaded successfully. Public URL: $publicUrl');
return publicUrl;
} catch (e, stackTrace) {
debugPrint('❌ Error uploading file to storage: $e');
debugPrint('Stack trace: $stackTrace');
return null;
}
}
/// Helper method to delete a file from Supabase Storage
Future<bool> deleteFileFromStorage(String fileUrl) async {
try {
debugPrint('🔄 Preparing to delete file from storage');
// Extract the file path from the full URL
final uri = Uri.parse(fileUrl);
final pathSegments = uri.pathSegments;
// Find the index of 'foto.aset' in the path
final fotoAsetIndex = pathSegments.indexWhere(
(segment) => segment == 'foto.aset',
);
if (fotoAsetIndex == -1 || fotoAsetIndex == pathSegments.length - 1) {
debugPrint(
'⚠️ Invalid file URL format, cannot extract file path: $fileUrl',
);
return false;
}
// Get the file path relative to the bucket
final filePath = pathSegments.sublist(fotoAsetIndex + 1).join('/');
debugPrint('🗑️ Deleting file from storage - Path: $filePath');
// Delete the file from storage
await client.storage.from('foto.aset').remove([filePath]);
debugPrint('✅ Successfully deleted file from storage');
return true;
} catch (e) {
debugPrint('❌ Error deleting file from storage: $e');
return false;
}
}
/// Updates the photos for an asset
/// Handles both local file uploads and existing URLs
/// Returns true if all operations were successful
Future<bool> updateFotoAset({
required String asetId,
required List<String> fotoUrls,
}) async {
if (fotoUrls.isEmpty) {
debugPrint(' No photos to update for asset: $asetId');
return true;
}
try {
debugPrint('🔄 Starting photo update for asset: $asetId');
// 1. Get existing photo URLs before deleting them
debugPrint('📋 Fetching existing photos for asset: $asetId');
final existingPhotos = await client
.from('foto_aset')
.select('foto_aset')
.eq('id_aset', asetId);
// 2. Delete files from storage first
if (existingPhotos is List && existingPhotos.isNotEmpty) {
debugPrint('🗑️ Deleting ${existingPhotos.length} files from storage');
for (final photo in existingPhotos) {
final url = photo['foto_aset'] as String?;
if (url != null && url.isNotEmpty) {
await deleteFileFromStorage(url);
} else {
debugPrint('⚠️ Skipping invalid photo URL: $photo');
}
}
} else {
debugPrint(' No existing photos found in database');
}
// 3. Remove duplicates from new fotoUrls
final uniqueFotoUrls = fotoUrls.toSet().toList();
debugPrint(
'📸 Processing ${uniqueFotoUrls.length} unique photos (was ${fotoUrls.length})',
);
// 4. Delete existing photo records from database
debugPrint('🗑️ Removing existing photo records from database');
try {
final deleteResponse = await client
.from('foto_aset')
.delete()
.eq('id_aset', asetId);
debugPrint('🗑️ Database delete response: $deleteResponse');
// Verify deletion
final remainingPhotos = await client
.from('foto_aset')
.select()
.eq('id_aset', asetId);
if (remainingPhotos is List && remainingPhotos.isNotEmpty) {
debugPrint(
'⚠️ Warning: ${remainingPhotos.length} photos still exist in database after delete',
);
}
} catch (e) {
debugPrint('❌ Error deleting existing photo records: $e');
// Continue with the update even if deletion fails
}
// 5. Process each unique new photo
bool allSuccess = true;
int processedCount = 0;
for (final fotoUrl in uniqueFotoUrls) {
if (fotoUrl.isEmpty) {
debugPrint('⏭️ Skipping empty photo URL');
continue;
}
try {
debugPrint(
'\n🔄 Processing photo ${processedCount + 1}/${uniqueFotoUrls.length}: ${fotoUrl.length > 50 ? '${fotoUrl.substring(0, 50)}...' : fotoUrl}',
);
// Check if it's a local file
if (fotoUrl.startsWith('file://') ||
fotoUrl.startsWith('/') ||
!fotoUrl.startsWith('http')) {
final file = File(fotoUrl.replaceFirst('file://', ''));
if (!await file.exists()) {
debugPrint('❌ File does not exist: ${file.path}');
allSuccess = false;
continue;
}
debugPrint('📤 Uploading local file...');
final uploadedUrl = await uploadFileToStorage(file);
if (uploadedUrl == null) {
debugPrint('❌ Failed to upload file');
allSuccess = false;
continue;
}
debugPrint('💾 Saving to database...');
final success = await addFotoAset(
asetId: asetId,
fotoUrl: uploadedUrl,
);
if (success) {
processedCount++;
debugPrint('✅ Successfully saved photo #$processedCount');
} else {
allSuccess = false;
debugPrint('❌ Failed to save photo URL to database');
}
}
// Skip placeholder values
else if (fotoUrl == 'pending_upload') {
debugPrint('⏭️ Skipping placeholder URL');
continue;
}
// Handle existing URLs
else if (fotoUrl.startsWith('http')) {
debugPrint('🌐 Processing existing URL...');
final success = await addFotoAset(asetId: asetId, fotoUrl: fotoUrl);
if (success) {
processedCount++;
debugPrint('✅ Successfully saved URL #$processedCount');
} else {
allSuccess = false;
debugPrint('❌ Failed to save URL to database');
}
} else {
debugPrint('⚠️ Unrecognized URL format, skipping');
}
} catch (e, stackTrace) {
allSuccess = false;
debugPrint('❌ Error processing photo: $e');
debugPrint('Stack trace: $stackTrace');
}
}
debugPrint('\n📊 Photo update complete');
debugPrint('✅ Success: $allSuccess');
debugPrint(
'📸 Processed: $processedCount/${uniqueFotoUrls.length} unique photos',
);
return allSuccess && processedCount > 0;
} catch (e) {
debugPrint('❌ Error updating foto aset: $e');
debugPrint('Stack trace: ${StackTrace.current}');
return false;
}
}
// Retrieve bookings for a specific asset on a specific date // Retrieve bookings for a specific asset on a specific date
Future<List<Map<String, dynamic>>> getAsetBookings( Future<List<Map<String, dynamic>>> getAsetBookings(
String asetId, String asetId,
@ -1061,7 +1468,9 @@ class AsetProvider extends GetxService {
.order('created_at'); .order('created_at');
if (response != null && response.isNotEmpty) { if (response != null && response.isNotEmpty) {
return response.map<String>((item) => item['foto_aset'] as String).toList(); return response
.map<String>((item) => item['foto_aset'] as String)
.toList();
} }
return []; return [];
} catch (e) { } catch (e) {
@ -1073,11 +1482,11 @@ class AsetProvider extends GetxService {
// Get items included in a package with additional asset details // Get items included in a package with additional asset details
Future<List<Map<String, dynamic>>> getPaketItems(String paketId) async { Future<List<Map<String, dynamic>>> getPaketItems(String paketId) async {
debugPrint('🔄 [1/3] Starting to fetch items for paket ID: $paketId'); debugPrint('🔄 [1/3] Starting to fetch items for paket ID: $paketId');
try { try {
// 1. First, get the basic package items (aset_id and kuantitas) // 1. First, get the basic package items (aset_id and kuantitas)
debugPrint('🔍 [2/3] Querying paket_item table for paket_id: $paketId'); debugPrint('🔍 [2/3] Querying paket_item table for paket_id: $paketId');
final response = await client final response = await client
.from('paket_item') .from('paket_item')
.select(''' .select('''
@ -1093,44 +1502,53 @@ class AsetProvider extends GetxService {
debugPrint('❌ [ERROR] Null response from paket_item query'); debugPrint('❌ [ERROR] Null response from paket_item query');
return []; return [];
} }
if (response.isEmpty) { if (response.isEmpty) {
debugPrint(' [INFO] No items found in paket_item for paket ID: $paketId'); debugPrint(
' [INFO] No items found in paket_item for paket ID: $paketId',
);
return []; return [];
} }
debugPrint('✅ [SUCCESS] Found ${response.length} items in paket_item'); debugPrint(
'✅ [SUCCESS] Found ${response.length} items in paket_item',
);
final List<Map<String, dynamic>> enrichedItems = []; final List<Map<String, dynamic>> enrichedItems = [];
// Process each item to fetch additional details // Process each item to fetch additional details
debugPrint('🔄 [3/3] Processing ${response.length} items to fetch asset details'); debugPrint(
'🔄 [3/3] Processing ${response.length} items to fetch asset details',
);
for (var item in response) { for (var item in response) {
final String? asetId = item['aset_id']?.toString(); final String? asetId = item['aset_id']?.toString();
final int kuantitas = item['kuantitas'] ?? 1; final int kuantitas = item['kuantitas'] ?? 1;
debugPrint('\n🔍 Processing item:'); debugPrint('\n🔍 Processing item:');
debugPrint(' - Raw item data: $item'); debugPrint(' - Raw item data: $item');
debugPrint(' - aset_id: $asetId'); debugPrint(' - aset_id: $asetId');
debugPrint(' - kuantitas: $kuantitas'); debugPrint(' - kuantitas: $kuantitas');
if (asetId == null || asetId.isEmpty) { if (asetId == null || asetId.isEmpty) {
debugPrint('⚠️ [WARNING] Skipping item with null/empty aset_id'); debugPrint('⚠️ [WARNING] Skipping item with null/empty aset_id');
continue; continue;
} }
try { try {
// 1. Get asset name from aset table // 1. Get asset name from aset table
debugPrint(' - Querying aset table for id: $asetId'); debugPrint(' - Querying aset table for id: $asetId');
final asetResponse = await client final asetResponse =
.from('aset') await client
.select('id, nama, deskripsi') .from('aset')
.eq('id', asetId) .select('id, nama, deskripsi')
.maybeSingle(); .eq('id', asetId)
.maybeSingle();
debugPrint(' - Aset response: ${asetResponse?.toString() ?? 'null'}');
debugPrint(
' - Aset response: ${asetResponse?.toString() ?? 'null'}',
);
if (asetResponse == null) { if (asetResponse == null) {
debugPrint('⚠️ [WARNING] No asset found with id: $asetId'); debugPrint('⚠️ [WARNING] No asset found with id: $asetId');
enrichedItems.add({ enrichedItems.add({
@ -1139,11 +1557,11 @@ class AsetProvider extends GetxService {
'nama_aset': 'Item tidak diketahui', 'nama_aset': 'Item tidak diketahui',
'foto_aset': '', 'foto_aset': '',
'semua_foto': <String>[], 'semua_foto': <String>[],
'error': 'Asset not found' 'error': 'Asset not found',
}); });
continue; continue;
} }
// 2. Get only the first photo from foto_aset table // 2. Get only the first photo from foto_aset table
debugPrint(' - Querying first photo for id_aset: $asetId'); debugPrint(' - Querying first photo for id_aset: $asetId');
final fotoResponse = await client final fotoResponse = await client
@ -1152,10 +1570,10 @@ class AsetProvider extends GetxService {
.eq('id_aset', asetId) .eq('id_aset', asetId)
.order('created_at', ascending: true) .order('created_at', ascending: true)
.limit(1); .limit(1);
String? fotoUtama = ''; String? fotoUtama = '';
List<String> semuaFoto = []; List<String> semuaFoto = [];
if (fotoResponse.isNotEmpty) { if (fotoResponse.isNotEmpty) {
final firstFoto = fotoResponse.first['foto_aset']?.toString(); final firstFoto = fotoResponse.first['foto_aset']?.toString();
if (firstFoto != null && firstFoto.isNotEmpty) { if (firstFoto != null && firstFoto.isNotEmpty) {
@ -1168,22 +1586,25 @@ class AsetProvider extends GetxService {
} else { } else {
debugPrint(' - No photos found for asset $asetId'); debugPrint(' - No photos found for asset $asetId');
} }
// 4. Combine all data // 4. Combine all data
final enrichedItem = { final enrichedItem = {
'aset_id': asetId, 'aset_id': asetId,
'kuantitas': kuantitas, 'kuantitas': kuantitas,
'nama_aset': asetResponse['nama']?.toString() ?? 'Nama tidak tersedia', 'nama_aset':
asetResponse['nama']?.toString() ?? 'Nama tidak tersedia',
'foto_aset': fotoUtama, 'foto_aset': fotoUtama,
'semua_foto': semuaFoto, 'semua_foto': semuaFoto,
'debug': { 'debug': {
'aset_query': asetResponse, 'aset_query': asetResponse,
'foto_count': semuaFoto.length 'foto_count': semuaFoto.length,
} },
}; };
debugPrint('✅ [ENRICHED ITEM] $enrichedItem');
enrichedItems.add(enrichedItem); enrichedItems.add(enrichedItem);
// Debug log // Debug log
debugPrint('✅ Successfully processed item:'); debugPrint('✅ Successfully processed item:');
debugPrint(' - Aset ID: $asetId'); debugPrint(' - Aset ID: $asetId');
@ -1193,7 +1614,6 @@ class AsetProvider extends GetxService {
if (semuaFoto.isNotEmpty) { if (semuaFoto.isNotEmpty) {
debugPrint(' - Foto Utama: ${semuaFoto.first}'); debugPrint(' - Foto Utama: ${semuaFoto.first}');
} }
} catch (e) { } catch (e) {
debugPrint('❌ Error processing asset $asetId: $e'); debugPrint('❌ Error processing asset $asetId: $e');
// Still add the basic item even if we couldn't fetch additional details // Still add the basic item even if we couldn't fetch additional details
@ -1206,10 +1626,14 @@ class AsetProvider extends GetxService {
}); });
} }
} }
debugPrint('✅ Successfully fetched ${enrichedItems.length} items with details'); debugPrint(
'✅ Successfully fetched ${enrichedItems.length} items with details:',
);
for (var item in enrichedItems) {
debugPrint(' - $item');
}
return enrichedItems; return enrichedItems;
} catch (e, stackTrace) { } catch (e, stackTrace) {
debugPrint('❌ Error getting package items for paket $paketId: $e'); debugPrint('❌ Error getting package items for paket $paketId: $e');
debugPrint('Stack trace: $stackTrace'); debugPrint('Stack trace: $stackTrace');
@ -1221,10 +1645,9 @@ class AsetProvider extends GetxService {
Future<List<Map<String, dynamic>>> getBankAccounts() async { Future<List<Map<String, dynamic>>> getBankAccounts() async {
try { try {
final response = await client final response = await client
.from('bank_accounts') .from('akun_bank')
.select('*') .select('*')
.eq('is_active', true) .order('nama_bank');
.order('bank_name');
if (response != null && response.isNotEmpty) { if (response != null && response.isNotEmpty) {
return List<Map<String, dynamic>>.from(response); return List<Map<String, dynamic>>.from(response);
@ -1235,4 +1658,325 @@ class AsetProvider extends GetxService {
return []; return [];
} }
} }
/// Fetch all packages with their related data (photos and rental time units)
Future<List<PaketModel>> getAllPaket() async {
final stopwatch = Stopwatch()..start();
final String debugId = DateTime.now().millisecondsSinceEpoch
.toString()
.substring(8);
void log(String message, {bool isError = false, bool isSection = false}) {
final prefix =
isError
? ''
: isSection
? '📌'
: ' ';
debugPrint('[$debugId] $prefix $message');
}
try {
log('🚀 Memulai pengambilan data paket...', isSection: true);
log('📡 Mengambil data paket dari database...');
// 1) Get all packages
final paketResponse = await client
.from('paket')
.select('*')
.order('created_at', ascending: false);
log('📥 Diterima ${paketResponse.length} paket dari database');
if (paketResponse.isEmpty) {
log(' Tidak ada paket yang ditemukan');
return [];
}
// Convert to list of PaketModel (without relations yet)
log('\n🔍 Memproses data paket...');
final List<PaketModel> paketList = [];
int successCount = 0;
for (var p in paketResponse) {
try {
final paket = PaketModel.fromMap(p as Map<String, dynamic>);
paketList.add(paket);
successCount++;
log(' ✅ Berhasil memproses paket: ${paket.id} - ${paket.nama}');
} catch (e) {
log('⚠️ Gagal memproses paket: $e', isError: true);
log(' Data paket: $p');
}
}
log('\n📊 Ringkasan Pemrosesan:');
log(' - Total data: ${paketResponse.length}');
log(' - Berhasil: $successCount');
log(' - Gagal: ${paketResponse.length - successCount}');
if (paketList.isEmpty) {
log(' Tidak ada paket yang valid setelah diproses');
return [];
}
// Kumpulkan semua ID paket
final List<String> paketIds = paketList.map((p) => p.id).toList();
log('\n📦 Mengambil data tambahan untuk ${paketList.length} paket...');
log(' ID Paket: ${paketIds.join(', ')}');
// 2) Ambil semua foto untuk paket-paket ini
log('\n🖼️ Mengambil data foto...');
final fotoResp = await client
.from('foto_aset')
.select('id_paket, foto_aset')
.inFilter('id_paket', paketIds);
log(' Ditemukan ${fotoResp.length} foto');
// Map packageId -> List<String> photos
final Map<String, List<String>> mapFoto = {};
int fotoCount = 0;
for (var row in fotoResp) {
try {
final pid = row['id_paket']?.toString() ?? '';
final url = row['foto_aset']?.toString() ?? '';
if (pid.isNotEmpty && url.isNotEmpty) {
mapFoto.putIfAbsent(pid, () => []).add(url);
fotoCount++;
} else {
log(' ⚠️ Data foto tidak valid: ${row.toString()}');
}
} catch (e) {
log('⚠️ Gagal memproses data foto: $e', isError: true);
}
}
log(' Berhasil memetakan $fotoCount foto ke ${mapFoto.length} paket');
// 3) Get all satuan_waktu_sewa for these packages
log('\n⏱️ Mengambil data satuan waktu sewa...');
final swsResp = await client
.from('satuan_waktu_sewa')
.select('paket_id, satuan_waktu_id, harga, maksimal_waktu')
.inFilter('paket_id', paketIds);
log(' Ditemukan ${swsResp.length} entri satuan waktu sewa');
// Process satuan waktu sewa
final Map<String, List<Map<String, dynamic>>> paketSatuanWaktu = {};
int swsCount = 0;
for (var row in swsResp) {
try {
final pid = row['paket_id']?.toString() ?? '';
if (pid.isNotEmpty) {
final swsData = {
'satuan_waktu_id': row['satuan_waktu_id'],
'harga': row['harga'],
'maksimal_waktu': row['maksimal_waktu'],
};
paketSatuanWaktu.putIfAbsent(pid, () => []).add(swsData);
swsCount++;
}
} catch (e) {
log('⚠️ Gagal memproses satuan waktu sewa: $e', isError: true);
log(' Data: $row');
}
}
log(
' Berhasil memetakan $swsCount satuan waktu ke ${paketSatuanWaktu.length} paket',
);
// 4) Gabungkan semua data
log('\n🔗 Menggabungkan data...');
final List<PaketModel> result = [];
int combinedCount = 0;
for (var paket in paketList) {
final pid = paket.id;
log('\n📦 Memproses paket: ${paket.nama} ($pid)');
try {
final updatedPaket = paket.copyWith();
// Lampirkan foto
if (mapFoto.containsKey(pid)) {
final fotoList = mapFoto[pid]!;
updatedPaket.images = List<String>.from(fotoList);
// Set foto utama jika belum ada
if (updatedPaket.images!.isNotEmpty &&
updatedPaket.foto_paket == null) {
updatedPaket.foto_paket = updatedPaket.images!.first;
log(' 📷 Menambahkan ${fotoList.length} foto');
log(' 🖼️ Foto utama: ${updatedPaket.foto_paket}');
}
} else {
log(' Tidak ada foto untuk paket ini');
}
// Lampirkan satuan waktu sewa
if (paketSatuanWaktu.containsKey(pid)) {
final swsList = List<Map<String, dynamic>>.from(
paketSatuanWaktu[pid] ?? [],
);
updatedPaket.satuanWaktuSewa = swsList;
log(' ⏱️ Menambahkan ${swsList.length} satuan waktu sewa');
// Log detail harga
for (var sws in swsList.take(2)) {
// Tampilkan maksimal 2 harga
log(
' - ${sws['harga']} / satuan waktu (ID: ${sws['satuan_waktu_id']})',
);
}
if (swsList.length > 2) {
log(' - ...dan ${swsList.length - 2} lainnya');
}
} else {
log(' Tidak ada satuan waktu sewa untuk paket ini');
}
result.add(updatedPaket);
combinedCount++;
log(' ✅ Berhasil memproses paket $pid');
} catch (e) {
log('⚠️ Gagal memproses paket $pid: $e', isError: true);
// Tetap tambahkan paket asli jika gagal diproses
result.add(paket);
}
}
// Ringkasan eksekusi
stopwatch.stop();
log('\n🎉 Selesai!', isSection: true);
log('📊 Ringkasan Eksekusi:');
log(' - Total paket: ${paketList.length}');
log(' - Berhasil diproses: $combinedCount/${paketList.length}');
log(' - Total foto: $fotoCount');
log(' - Total satuan waktu: $swsCount');
log(' - Waktu eksekusi: ${stopwatch.elapsedMilliseconds}ms');
log(' - ID Debug: $debugId');
return result;
} catch (e, stackTrace) {
log('\n❌ ERROR KRITIS', isError: true);
log('Pesan error: $e', isError: true);
log('Stack trace: $stackTrace', isError: true);
log('ID Debug: $debugId', isError: true);
rethrow;
debugPrint('❌ [getAllPaket] Error: $e');
debugPrint('Stack trace: $stackTrace');
rethrow;
}
}
// Update tagihan_dibayar and insert pembayaran
Future<bool> processPembayaranTagihan({
required String tagihanSewaId,
required int nominal,
required String metodePembayaran,
}) async {
try {
// 1. Get current tagihan_dibayar
final tagihan =
await client
.from('tagihan_sewa')
.select('tagihan_dibayar')
.eq('id', tagihanSewaId)
.maybeSingle();
int currentDibayar = 0;
if (tagihan != null && tagihan['tagihan_dibayar'] != null) {
currentDibayar =
int.tryParse(tagihan['tagihan_dibayar'].toString()) ?? 0;
}
final newDibayar = currentDibayar + nominal;
// 2. Update tagihan_dibayar
await client
.from('tagihan_sewa')
.update({'tagihan_dibayar': newDibayar})
.eq('id', tagihanSewaId);
// 3. Insert pembayaran
final authProvider = Get.find<AuthProvider>();
final idPetugas = authProvider.getCurrentUserId();
final pembayaranData = {
'tagihan_sewa_id': tagihanSewaId,
'metode_pembayaran': metodePembayaran,
'total_pembayaran': nominal,
'status': 'lunas',
'created_at': DateTime.now().toIso8601String(),
'id_petugas': idPetugas,
};
await client.from('pembayaran').insert(pembayaranData);
return true;
} catch (e) {
debugPrint('❌ Error processing pembayaran tagihan: $e');
return false;
}
}
// Update status of sewa_aset by ID
Future<bool> updateSewaAsetStatus({
required String sewaAsetId,
required String status,
}) async {
try {
debugPrint('🔄 Updating status of sewa_aset ID: $sewaAsetId to $status');
final response = await client
.from('sewa_aset')
.update({'status': status})
.eq('id', sewaAsetId);
debugPrint('✅ Status updated for sewa_aset ID: $sewaAsetId');
return true;
} catch (e) {
debugPrint('❌ Error updating sewa_aset status: $e');
return false;
}
}
// Get all payment proof image URLs for a sewa_aset (by tagihan_sewa)
Future<List<String>> getFotoPembayaranUrlsByTagihanSewaId(
String sewaAsetId,
) async {
try {
// 1. Get tagihan_sewa by sewaAsetId
final tagihan = await getTagihanSewa(sewaAsetId);
if (tagihan == null || tagihan['id'] == null) return [];
final tagihanSewaId = tagihan['id'];
// 2. Fetch all foto_pembayaran for this tagihan_sewa_id
final List<dynamic> response = await client
.from('foto_pembayaran')
.select('foto_pembayaran')
.eq('tagihan_sewa_id', tagihanSewaId)
.order('created_at', ascending: false);
// 3. Extract URLs
return response
.map<String>((row) => row['foto_pembayaran']?.toString() ?? '')
.where((url) => url.isNotEmpty)
.toList();
} catch (e) {
debugPrint('❌ Error fetching foto pembayaran: $e');
return [];
}
}
Future<int> countSewaAsetByStatus(List<String> statuses) async {
// Supabase expects the IN filter as a comma-separated string in parentheses
final statusString = '(${statuses.map((s) => '"$s"').join(',')})';
final response = await client
.from('sewa_aset')
.select('id')
.filter('status', 'in', statusString);
if (response is List) {
return response.length;
}
return 0;
}
} }

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

@ -8,6 +8,10 @@ class AuthController extends GetxController {
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 +19,7 @@ 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;
// Form status // Form status
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
@ -28,6 +33,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) {
@ -172,6 +181,8 @@ class AuthController extends GetxController {
void onClose() { void onClose() {
emailController.dispose(); emailController.dispose();
passwordController.dispose(); passwordController.dispose();
nameController.dispose();
confirmPasswordController.dispose();
super.onClose(); super.onClose();
} }
@ -181,7 +192,8 @@ class AuthController extends GetxController {
if (email.value.isEmpty || if (email.value.isEmpty ||
password.value.isEmpty || password.value.isEmpty ||
nik.value.isEmpty || nik.value.isEmpty ||
phoneNumber.value.isEmpty) { phoneNumber.value.isEmpty ||
alamatLengkap.value.isEmpty) {
errorMessage.value = 'Semua field harus diisi'; errorMessage.value = 'Semua field harus diisi';
return; return;
} }
@ -222,6 +234,7 @@ class AuthController extends GetxController {
data: { data: {
'nik': nik.value.trim(), 'nik': nik.value.trim(),
'phone_number': phoneNumber.value.trim(), 'phone_number': phoneNumber.value.trim(),
'alamat_lengkap': alamatLengkap.value.trim(),
'role': selectedRole.value, 'role': selectedRole.value,
}, },
); );

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

@ -30,12 +30,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,
),
), ),
), ),
), ),
@ -89,7 +85,6 @@ class LoginView extends GetView<AuthController> {
_buildHeader(), _buildHeader(),
const SizedBox(height: 40), const SizedBox(height: 40),
_buildLoginCard(), _buildLoginCard(),
const SizedBox(height: 24),
_buildRegisterLink(), _buildRegisterLink(),
const SizedBox(height: 30), const SizedBox(height: 30),
], ],
@ -161,7 +156,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,7 +199,6 @@ class LoginView extends GetView<AuthController> {
), ),
), ),
), ),
const SizedBox(height: 32),
// Login button // Login button
Obx( Obx(

View File

@ -187,7 +187,7 @@ class RegistrationView extends GetView<AuthController> {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'Pendaftaran hanya dapat dilakukan oleh warga dan mitra yang sudah terverivikasi. Silahkan hubungi petugas atau kunjungi kantor untuk informasi lebih lanjut.', 'Setelah pendaftaran lengkapi data diri untuk dapat melakukan sewa',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: AppColors.textPrimary, color: AppColors.textPrimary,
@ -203,121 +203,32 @@ class RegistrationView extends GetView<AuthController> {
} }
Widget _buildRegistrationForm() { Widget _buildRegistrationForm() {
return Column( return Form(
crossAxisAlignment: CrossAxisAlignment.start, key: controller.formKey,
children: [ child: Column(
// Email Input crossAxisAlignment: CrossAxisAlignment.stretch,
_buildInputLabel('Email'), children: [
const SizedBox(height: 8), _buildInputLabel('Email'),
_buildEmailField(), _buildEmailField(),
const SizedBox(height: 20), const SizedBox(height: 16),
_buildInputLabel('Password'),
// Password Input _buildPasswordField(),
_buildInputLabel('Password'), const SizedBox(height: 16),
const SizedBox(height: 8), _buildInputLabel('Konfirmasi Password'),
_buildPasswordField(), _buildConfirmPasswordField(),
const SizedBox(height: 20), const SizedBox(height: 16),
_buildInputLabel('Nama Lengkap'),
// NIK Input _buildNameField(),
_buildInputLabel('NIK'), const SizedBox(height: 16),
const SizedBox(height: 8), _buildInputLabel('No HP'),
_buildNikField(), _buildPhoneField(),
const SizedBox(height: 20), const SizedBox(height: 16),
_buildInputLabel('Alamat Lengkap'),
// Phone Number Input _buildAlamatField(),
_buildInputLabel('No. Hp'), const SizedBox(height: 16),
const SizedBox(height: 8), // Removed: NIK, No HP, and Dropdown Daftar Sebagai
_buildPhoneField(), ],
const SizedBox(height: 20), ),
// Role Selection Dropdown
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Daftar Sebagai',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!, width: 1),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Obx(
() => DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
value: controller.selectedRole.value,
hint: const Text('Pilih Peran'),
items: [
DropdownMenuItem(
value: 'WARGA',
child: const Text('Warga'),
),
DropdownMenuItem(
value: 'PETUGAS_MITRA',
child: const Text('Mitra'),
),
],
onChanged: (value) {
controller.setRole(value);
},
icon: const Icon(Icons.arrow_drop_down),
style: const TextStyle(
color: Colors.black87,
fontSize: 14,
),
),
),
),
),
),
],
),
const SizedBox(height: 20),
// Error message
Obx(
() =>
controller.errorMessage.value.isNotEmpty
? Container(
margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.errorLight,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(
Icons.error_outline,
color: AppColors.error,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
controller.errorMessage.value,
style: TextStyle(
color: AppColors.error,
fontSize: 13,
),
),
),
],
),
)
: const SizedBox.shrink(),
),
],
); );
} }
@ -415,78 +326,101 @@ class RegistrationView extends GetView<AuthController> {
); );
} }
Widget _buildNikField() { Widget _buildConfirmPasswordField() {
return Container( return Obx(
decoration: BoxDecoration( () => TextFormField(
color: AppColors.surface, controller: controller.confirmPasswordController,
borderRadius: BorderRadius.circular(16), obscureText: !controller.isConfirmPasswordVisible.value,
boxShadow: [
BoxShadow(
color: AppColors.shadow,
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: TextField(
onChanged: (value) => controller.nik.value = value,
keyboardType: TextInputType.number,
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Masukkan NIK anda', hintText: 'Masukkan ulang password anda',
hintStyle: TextStyle(color: AppColors.textLight), suffixIcon: IconButton(
prefixIcon: Icon( icon: Icon(
Icons.credit_card_outlined, controller.isConfirmPasswordVisible.value
color: AppColors.primary, ? Icons.visibility
: Icons.visibility_off,
),
onPressed: controller.toggleConfirmPasswordVisibility,
), ),
border: InputBorder.none, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(vertical: 16), contentPadding: const EdgeInsets.symmetric(
enabledBorder: OutlineInputBorder( horizontal: 16,
borderRadius: BorderRadius.circular(16), vertical: 14,
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
), ),
), ),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Konfirmasi password tidak boleh kosong';
}
if (value != controller.passwordController.text) {
return 'Password tidak cocok';
}
return null;
},
), ),
); );
} }
Widget _buildPhoneField() { Widget _buildNameField() {
return Container( return TextFormField(
decoration: BoxDecoration( controller: controller.nameController,
color: AppColors.surface, decoration: InputDecoration(
borderRadius: BorderRadius.circular(16), hintText: 'Masukkan nama lengkap anda',
boxShadow: [ border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
BoxShadow( contentPadding: const EdgeInsets.symmetric(
color: AppColors.shadow, horizontal: 16,
blurRadius: 8, vertical: 14,
offset: const Offset(0, 2),
),
],
),
child: TextField(
onChanged: (value) => controller.phoneNumber.value = value,
keyboardType: TextInputType.phone,
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
decoration: InputDecoration(
hintText: 'Masukkan nomor HP anda',
hintStyle: TextStyle(color: AppColors.textLight),
prefixIcon: Icon(Icons.phone_outlined, color: AppColors.primary),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 16),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
),
), ),
), ),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nama lengkap tidak boleh kosong';
}
return null;
},
);
}
Widget _buildPhoneField() {
return TextFormField(
keyboardType: TextInputType.phone,
decoration: InputDecoration(
hintText: 'Masukkan nomor HP anda',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
onChanged: (value) => controller.phoneNumber.value = value,
validator: (value) {
if (value == null || value.isEmpty) {
return 'No HP tidak boleh kosong';
}
if (!value.startsWith('08') || value.length < 10) {
return 'Nomor HP tidak valid (harus diawali 08 dan minimal 10 digit)';
}
return null;
},
);
}
Widget _buildAlamatField() {
return TextFormField(
decoration: InputDecoration(
hintText: 'Masukkan alamat lengkap anda',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
onChanged: (value) => controller.alamatLengkap.value = value,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Alamat lengkap tidak boleh kosong';
}
return null;
},
); );
} }

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

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

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

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

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

View File

@ -1,6 +1,10 @@
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 '../../../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,6 +12,8 @@ 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 = 'Rp 8.500.000'.obs;
@ -20,7 +26,7 @@ class PetugasBumdesDashboardController extends GetxController {
final persentaseSewa = 100.obs; final persentaseSewa = 100.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 = 5.obs;
@ -43,42 +49,128 @@ class PetugasBumdesDashboardController extends GetxController {
final tagihanAktifCountSewa = 7.obs; final tagihanAktifCountSewa = 7.obs;
final periksaPembayaranCountSewa = 2.obs; final periksaPembayaranCountSewa = 2.obs;
// Statistik pendapatan
final totalPendapatan = 0.obs;
final pendapatanBulanIni = 0.obs;
final pendapatanBulanLalu = 0.obs;
final pendapatanTunai = 0.obs;
final pendapatanTransfer = 0.obs;
final trenPendapatan = <int>[].obs; // 6 bulan terakhir
// Dashboard statistics
final pembayaranStats = <String, dynamic>{}.obs;
final isStatsLoading = true.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
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');
} }
// 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 = '';
}
}
void changeTab(int index) { void changeTab(int index) {
try { try {

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; // State
final RxBool isLoading = false.obs;
// Kategori untuk filter final RxString searchQuery = ''.obs;
final categories = <String>[ final RxString selectedCategory = 'Semua'.obs;
'Semua', final RxString sortBy = 'Terbaru'.obs;
'Pesta', final RxList<PaketModel> packages = <PaketModel>[].obs;
'Rapat', final RxList<PaketModel> filteredPackages = <PaketModel>[].obs;
'Olahraga',
'Pernikahan', // Sort options for the dropdown
'Lainnya', final List<String> sortOptions = [
];
// Opsi pengurutan
final sortOptions = <String>[
'Terbaru', 'Terbaru',
'Terlama', 'Terlama',
'Harga Tertinggi', 'Harga Tertinggi',
@ -26,175 +26,221 @@ class PetugasPaketController extends GetxController {
'Nama A-Z', 'Nama A-Z',
'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();
} // Initialize logger
_logger = Logger(
// Format harga ke Rupiah printer: PrettyPrinter(
String formatPrice(int price) { methodCount: 0,
final formatter = NumberFormat.currency( errorMethodCount: 5,
locale: 'id', colors: true,
symbol: 'Rp ', printEmojis: true,
decimalDigits: 0, ),
); );
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 {
isLoading.value = true; try {
await Future.delayed(const Duration(milliseconds: 800)); // Simulasi loading isLoading.value = true;
_logger.i('🔄 [fetchPackages] Fetching packages...');
paketList.value = [
{ final result = await _asetProvider.getAllPaket();
'id': '1',
'nama': 'Paket Pesta Ulang Tahun', if (result.isEmpty) {
'kategori': 'Pesta', _logger.w(' [fetchPackages] No packages found');
'harga': 500000, packages.clear();
'deskripsi': filteredPackages.clear();
'Paket lengkap untuk acara ulang tahun. Termasuk 5 meja, 20 kursi, backdrop, dan sound system.', return;
'tersedia': true, }
'created_at': '2023-08-10',
'items': [ packages.assignAll(result);
{'nama': 'Meja Panjang', 'jumlah': 5}, filteredPackages.assignAll(result);
{'nama': 'Kursi Plastik', 'jumlah': 20},
{'nama': 'Sound System', 'jumlah': 1}, // Update legacy list for backward compatibility
{'nama': 'Backdrop', 'jumlah': 1}, _updateLegacyPaketList();
],
'gambar': 'https://example.com/images/paket_ultah.jpg', _logger.i('✅ [fetchPackages] Successfully loaded ${result.length} packages');
},
{ } catch (e, stackTrace) {
'id': '2', _logger.e('❌ [fetchPackages] Error fetching packages',
'nama': 'Paket Rapat Sedang', error: e,
'kategori': 'Rapat', stackTrace: stackTrace);
'harga': 300000,
'deskripsi': Get.snackbar(
'Paket untuk rapat sedang. Termasuk 1 meja rapat besar, 10 kursi, proyektor, dan screen.', 'Error',
'tersedia': true, 'Gagal memuat data paket. Silakan coba lagi.',
'created_at': '2023-09-05', snackPosition: SnackPosition.BOTTOM,
'items': [ backgroundColor: Colors.red,
{'nama': 'Meja Rapat', 'jumlah': 1}, colorText: Colors.white,
{'nama': 'Kursi Kantor', 'jumlah': 10}, );
{'nama': 'Proyektor', 'jumlah': 1}, } finally {
{'nama': 'Screen', 'jumlah': 1}, isLoading.value = false;
],
'gambar': 'https://example.com/images/paket_rapat.jpg',
},
{
'id': '3',
'nama': 'Paket Pesta Pernikahan',
'kategori': 'Pernikahan',
'harga': 1500000,
'deskripsi':
'Paket lengkap untuk acara pernikahan. Termasuk 20 meja, 100 kursi, sound system, dekorasi, dan tenda.',
'tersedia': true,
'created_at': '2023-10-12',
'items': [
{'nama': 'Meja Bundar', 'jumlah': 20},
{'nama': 'Kursi Tamu', 'jumlah': 100},
{'nama': 'Sound System Besar', 'jumlah': 1},
{'nama': 'Tenda 10x10', 'jumlah': 2},
{'nama': 'Set Dekorasi Pengantin', 'jumlah': 1},
],
'gambar': 'https://example.com/images/paket_nikah.jpg',
},
{
'id': '4',
'nama': 'Paket Olahraga Voli',
'kategori': 'Olahraga',
'harga': 200000,
'deskripsi':
'Paket perlengkapan untuk turnamen voli. Termasuk net, bola, dan tiang voli.',
'tersedia': false,
'created_at': '2023-07-22',
'items': [
{'nama': 'Net Voli', 'jumlah': 1},
{'nama': 'Bola Voli', 'jumlah': 3},
{'nama': 'Tiang Voli', 'jumlah': 2},
],
'gambar': 'https://example.com/images/paket_voli.jpg',
},
{
'id': '5',
'nama': 'Paket Pesta Anak',
'kategori': 'Pesta',
'harga': 350000,
'deskripsi':
'Paket untuk pesta ulang tahun anak-anak. Termasuk 3 meja, 15 kursi, dekorasi tema, dan sound system kecil.',
'tersedia': true,
'created_at': '2023-11-01',
'items': [
{'nama': 'Meja Anak', 'jumlah': 3},
{'nama': 'Kursi Anak', 'jumlah': 15},
{'nama': 'Set Dekorasi Tema', 'jumlah': 1},
{'nama': 'Sound System Kecil', 'jumlah': 1},
],
'gambar': 'https://example.com/images/paket_anak.jpg',
},
];
filterPaket();
isLoading.value = false;
}
// Filter paket berdasarkan search query dan kategori
void filterPaket() {
filteredPaketList.value =
paketList.where((paket) {
final matchesQuery =
paket['nama'].toString().toLowerCase().contains(
searchQuery.value.toLowerCase(),
) ||
paket['deskripsi'].toString().toLowerCase().contains(
searchQuery.value.toLowerCase(),
);
final matchesCategory =
selectedCategory.value == 'Semua' ||
paket['kategori'] == selectedCategory.value;
return matchesQuery && matchesCategory;
}).toList();
// Sort the filtered list
sortFilteredList();
}
// Sort the filtered list
void sortFilteredList() {
switch (sortBy.value) {
case 'Terbaru':
filteredPaketList.sort(
(a, b) => b['created_at'].compareTo(a['created_at']),
);
break;
case 'Terlama':
filteredPaketList.sort(
(a, b) => a['created_at'].compareTo(b['created_at']),
);
break;
case 'Harga Tertinggi':
filteredPaketList.sort((a, b) => b['harga'].compareTo(a['harga']));
break;
case 'Harga Terendah':
filteredPaketList.sort((a, b) => a['harga'].compareTo(b['harga']));
break;
case 'Nama A-Z':
filteredPaketList.sort((a, b) => a['nama'].compareTo(b['nama']));
break;
case 'Nama Z-A':
filteredPaketList.sort((a, b) => b['nama'].compareTo(a['nama']));
break;
} }
} }
/// Update legacy paketList for backward compatibility
void _updateLegacyPaketList() {
try {
_logger.d('🔄 [_updateLegacyPaketList] Updating legacy paketList...');
final List<Map<String, dynamic>> legacyList = packages.map((pkg) {
return {
'id': pkg.id,
'nama': pkg.nama,
'deskripsi': pkg.deskripsi,
'harga': pkg.harga,
'kuantitas': pkg.kuantitas,
'status': pkg.status, // Add status to legacy mapping
'foto': pkg.foto,
'foto_paket': pkg.foto_paket,
'images': pkg.images,
'satuanWaktuSewa': pkg.satuanWaktuSewa,
'created_at': pkg.createdAt,
'updated_at': pkg.updatedAt,
};
}).toList();
paketList.assignAll(legacyList);
filteredPaketList.assignAll(legacyList);
_logger.d('✅ [_updateLegacyPaketList] Updated ${legacyList.length} packages');
} catch (e, stackTrace) {
_logger.e('❌ [_updateLegacyPaketList] Error updating legacy list',
error: e,
stackTrace: stackTrace);
}
}
/// For backward compatibility
Future<void> loadPaketData() async {
_logger.d(' [loadPaketData] Using fetchPackages() instead');
await fetchPackages();
}
/// Filter packages based on search query and category
void filterPaket() {
try {
_logger.d('🔄 [filterPaket] Filtering packages...');
if (searchQuery.value.isEmpty && selectedCategory.value == 'Semua') {
filteredPackages.value = List.from(packages);
filteredPaketList.value = List.from(paketList);
} else {
// Filter new packages
filteredPackages.value = packages.where((paket) {
final matchesSearch = searchQuery.value.isEmpty ||
paket.nama.toLowerCase().contains(searchQuery.value.toLowerCase());
// For now, we're not using categories in the new model
// You can add category filtering if needed
final matchesCategory = selectedCategory.value == 'Semua';
return matchesSearch && matchesCategory;
}).toList();
// Also update legacy list for backward compatibility
filteredPaketList.value = paketList.where((paket) {
final matchesSearch = searchQuery.value.isEmpty ||
(paket['nama']?.toString() ?? '').toLowerCase()
.contains(searchQuery.value.toLowerCase());
// For legacy support, check if category exists
final matchesCategory = selectedCategory.value == 'Semua' ||
(paket['kategori']?.toString() ?? '') == selectedCategory.value;
return matchesSearch && matchesCategory;
}).toList();
}
sortFilteredList();
_logger.d('✅ [filterPaket] Filtered to ${filteredPackages.length} packages');
} catch (e, stackTrace) {
_logger.e('❌ [filterPaket] Error filtering packages',
error: e,
stackTrace: stackTrace);
}
}
/// Sort the filtered list based on the selected sort option
void sortFilteredList() {
try {
_logger.d('🔄 [sortFilteredList] Sorting packages by ${sortBy.value}');
// Sort new packages
switch (sortBy.value) {
case 'Terbaru':
filteredPackages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
break;
case 'Terlama':
filteredPackages.sort((a, b) => a.createdAt.compareTo(b.createdAt));
break;
case 'Harga Tertinggi':
filteredPackages.sort((a, b) => b.harga.compareTo(a.harga));
break;
case 'Harga Terendah':
filteredPackages.sort((a, b) => a.harga.compareTo(b.harga));
break;
case 'Nama A-Z':
filteredPackages.sort((a, b) => a.nama.compareTo(b.nama));
break;
case 'Nama Z-A':
filteredPackages.sort((a, b) => b.nama.compareTo(a.nama));
break;
}
// Also sort legacy list for backward compatibility
switch (sortBy.value) {
case 'Terbaru':
filteredPaketList.sort((a, b) =>
((b['created_at'] ?? '') as String).compareTo((a['created_at'] ?? '') as String));
break;
case 'Terlama':
filteredPaketList.sort((a, b) =>
((a['created_at'] ?? '') as String).compareTo((b['created_at'] ?? '') as String));
break;
case 'Harga Tertinggi':
filteredPaketList.sort((a, b) =>
((b['harga'] ?? 0) as int).compareTo((a['harga'] ?? 0) as int));
break;
case 'Harga Terendah':
filteredPaketList.sort((a, b) =>
((a['harga'] ?? 0) as int).compareTo((b['harga'] ?? 0) as int));
break;
case 'Nama A-Z':
filteredPaketList.sort((a, b) =>
((a['nama'] ?? '') as String).compareTo((b['nama'] ?? '') as String));
break;
case 'Nama Z-A':
filteredPaketList.sort((a, b) =>
((b['nama'] ?? '') as String).compareTo((a['nama'] ?? '') as String));
break;
}
_logger.d('✅ [sortFilteredList] Sorted ${filteredPackages.length} packages');
} catch (e, stackTrace) {
_logger.e('❌ [sortFilteredList] Error sorting packages',
error: e,
stackTrace: stackTrace);
}
}
// Set search query dan filter paket // Set search query dan filter paket
void setSearchQuery(String query) { void setSearchQuery(String query) {
searchQuery.value = query; searchQuery.value = query;
@ -214,40 +260,134 @@ 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 {
filterPaket(); isLoading.value = true;
Get.back();
Get.snackbar( // Convert to PaketModel
'Sukses', final newPaket = PaketModel.fromJson({
'Paket baru berhasil ditambahkan', ...paketData,
snackPosition: SnackPosition.BOTTOM, 'id': DateTime.now().millisecondsSinceEpoch.toString(),
); 'created_at': DateTime.now().toIso8601String(),
} 'updated_at': DateTime.now().toIso8601String(),
});
// Edit paket
void editPaket(String id, Map<String, dynamic> updatedPaket) { // Add to the list
final index = paketList.indexWhere((element) => element['id'] == id); packages.add(newPaket);
if (index >= 0) { _updateLegacyPaketList();
paketList[index] = updatedPaket;
filterPaket(); filterPaket();
Get.back(); Get.back();
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Paket berhasil diperbarui', 'Paket baru berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
); );
} catch (e, stackTrace) {
_logger.e('❌ [addPaket] Error adding package',
error: e,
stackTrace: stackTrace);
Get.snackbar(
'Error',
'Gagal menambahkan paket. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
// Edit paket
Future<void> editPaket(String id, Map<String, dynamic> updatedData) async {
try {
isLoading.value = true;
final index = packages.indexWhere((pkg) => pkg.id == id);
if (index >= 0) {
// Update the package
final updatedPaket = packages[index].copyWith(
nama: updatedData['nama']?.toString() ?? packages[index].nama,
deskripsi: updatedData['deskripsi']?.toString() ?? packages[index].deskripsi,
kuantitas: (updatedData['kuantitas'] is int)
? updatedData['kuantitas']
: (int.tryParse(updatedData['kuantitas']?.toString() ?? '0') ?? packages[index].kuantitas),
updatedAt: DateTime.now(),
);
packages[index] = updatedPaket;
_updateLegacyPaketList();
filterPaket();
Get.back();
Get.snackbar(
'Sukses',
'Paket berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
}
} catch (e, stackTrace) {
_logger.e('❌ [editPaket] Error updating package',
error: e,
stackTrace: stackTrace);
Get.snackbar(
'Error',
'Gagal memperbarui paket. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
} }
} }
// Hapus paket // Hapus paket
void deletePaket(String id) { Future<void> deletePaket(String id) async {
paketList.removeWhere((element) => element['id'] == id); try {
filterPaket(); isLoading.value = true;
Get.snackbar(
'Sukses', // Remove from the main list
'Paket berhasil dihapus', packages.removeWhere((pkg) => pkg.id == id);
snackPosition: SnackPosition.BOTTOM, _updateLegacyPaketList();
); filterPaket();
Get.back();
Get.snackbar(
'Sukses',
'Paket berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e, stackTrace) {
_logger.e('❌ [deletePaket] Error deleting package',
error: e,
stackTrace: stackTrace);
Get.snackbar(
'Error',
'Gagal menghapus paket. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
/// Format price to Rupiah currency
String formatPrice(num price) {
return 'Rp ${NumberFormat('#,##0', 'id_ID').format(price)}';
} }
} }

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,25 +50,21 @@ 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();
} }
@ -68,100 +73,8 @@ 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)); sewaList.assignAll(data);
// Populate with mock data
sewaList.assignAll([
{
'id': '1',
'order_id': 'SWA-001',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-02-05',
'tanggal_selesai': '2025-02-10',
'total_biaya': 45000,
'status': 'Diterima',
'photo_url': 'https://example.com/photo1.jpg',
},
{
'id': '2',
'order_id': 'SWA-002',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-02-15',
'tanggal_selesai': '2025-02-20',
'total_biaya': 30000,
'status': 'Selesai',
'photo_url': 'https://example.com/photo2.jpg',
},
{
'id': '3',
'order_id': 'SWA-003',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-02-25',
'tanggal_selesai': '2025-03-01',
'total_biaya': 35000,
'status': 'Menunggu Pembayaran',
'photo_url': 'https://example.com/photo3.jpg',
},
{
'id': '4',
'order_id': 'SWA-004',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-03-05',
'tanggal_selesai': '2025-03-08',
'total_biaya': 20000,
'status': 'Periksa Pembayaran',
'photo_url': 'https://example.com/photo4.jpg',
},
{
'id': '5',
'order_id': 'SWA-005',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-03-12',
'tanggal_selesai': '2025-03-14',
'total_biaya': 15000,
'status': 'Dibatalkan',
'photo_url': 'https://example.com/photo5.jpg',
},
{
'id': '6',
'order_id': 'SWA-006',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-03-18',
'tanggal_selesai': '2025-03-20',
'total_biaya': 25000,
'status': 'Pembayaran Denda',
'photo_url': 'https://example.com/photo6.jpg',
},
{
'id': '7',
'order_id': 'SWA-007',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-03-25',
'tanggal_selesai': '2025-03-28',
'total_biaya': 40000,
'status': 'Periksa Denda',
'photo_url': 'https://example.com/photo7.jpg',
},
{
'id': '8',
'order_id': 'SWA-008',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-04-02',
'tanggal_selesai': '2025-04-05',
'total_biaya': 10000,
'status': 'Dikembalikan',
'photo_url': 'https://example.com/photo8.jpg',
},
]);
} catch (e) { } catch (e) {
print('Error loading sewa data: $e'); print('Error loading sewa data: $e');
} finally { } finally {
@ -196,10 +109,11 @@ 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;
@ -213,102 +127,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;
} }
} }
// Get icon based on status
IconData getStatusIcon(String status) {
switch (status) {
case 'MENUNGGU PEMBAYARAN':
return Icons.payments_outlined;
case 'PERIKSA PEMBAYARAN':
return Icons.fact_check_outlined;
case 'DITERIMA':
return Icons.check_circle_outlined;
case 'AKTIF':
return Icons.play_circle_outline;
case 'PEMBYARAN DENDA':
return Icons.money_off_csred_outlined;
case 'PERIKSA PEMBAYARAN DENDA':
return Icons.assignment_late_outlined;
case 'DIKEMBALIKAN':
return Icons.assignment_return_outlined;
case 'SELESAI':
return Icons.task_alt_outlined;
case 'DIBATALKAN':
return Icons.cancel_outlined;
default:
return Icons.help_outline_rounded;
}
}
// Handle sewa approval (from "Periksa Pembayaran" to "Diterima") // Handle sewa approval (from "Periksa Pembayaran" to "Diterima")
void approveSewa(String id) { void approveSewa(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];
final currentStatus = sewa['status']; final currentStatus = sewa.status;
String? newStatus;
if (currentStatus == 'Periksa Pembayaran') { if (currentStatus == 'PERIKSA PEMBAYARAN') {
sewa['status'] = 'Diterima'; newStatus = 'DITERIMA';
} else if (currentStatus == 'Periksa Denda') { } else if (currentStatus == 'PERIKSA PEMBAYARAN DENDA') {
sewa['status'] = 'Selesai'; newStatus = 'SELESAI';
} else if (currentStatus == 'Menunggu Pembayaran') { } else if (currentStatus == 'MENUNGGU PEMBAYARAN') {
sewa['status'] = 'Periksa Pembayaran'; newStatus = 'PERIKSA PEMBAYARAN';
}
if (newStatus != null) {
sewaList[index] = SewaModel(
id: sewa.id,
userId: sewa.userId,
status: newStatus,
waktuMulai: sewa.waktuMulai,
waktuSelesai: sewa.waktuSelesai,
tanggalPemesanan: sewa.tanggalPemesanan,
tipePesanan: sewa.tipePesanan,
kuantitas: sewa.kuantitas,
asetId: sewa.asetId,
asetNama: sewa.asetNama,
asetFoto: sewa.asetFoto,
paketId: sewa.paketId,
paketNama: sewa.paketNama,
paketFoto: sewa.paketFoto,
totalTagihan: sewa.totalTagihan,
wargaNama: sewa.wargaNama,
wargaNoHp: sewa.wargaNoHp,
wargaAvatar: sewa.wargaAvatar,
);
sewaList.refresh();
} }
sewaList[index] = sewa;
sewaList.refresh();
} }
} }
// Handle sewa rejection or cancellation // 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,187 @@
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';
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.BOTTOM,
);
// Optionally navigate back if in edit mode without an ID
Future.delayed(Duration.zero, () => Get.back());
}
} else {
// Set default values for new asset
debugPrint('[DEBUG] Add new asset mode');
quantityController.text = '1';
unitOfMeasureController.text = 'Unit';
}
} else {
// Default values for new asset when no arguments are passed
debugPrint('[DEBUG] No arguments passed, defaulting to add new asset mode');
quantityController.text = '1';
unitOfMeasureController.text = 'Unit';
}
} catch (e, stackTrace) {
debugPrint('[ERROR] Error in onInit: $e');
debugPrint('Stack trace: $stackTrace');
// Ensure loading is set to false even if there's an error
isLoading.value = false;
Get.snackbar(
'Error',
'Terjadi kesalahan saat memuat data',
snackPosition: SnackPosition.BOTTOM,
);
}
// Listen to field changes for validation
nameController.addListener(validateForm);
descriptionController.addListener(validateForm);
quantityController.addListener(validateForm);
pricePerHourController.addListener(validateForm);
pricePerDayController.addListener(validateForm);
}
final AsetProvider _asetProvider = Get.find<AsetProvider>();
final isLoading = false.obs;
Future<void> _loadAssetData(String assetId) async {
try {
isLoading.value = true;
debugPrint('[DEBUG] Fetching asset data for ID: $assetId');
// Fetch asset data from Supabase
final aset = await _asetProvider.getAsetById(assetId);
if (aset == null) {
throw Exception('Aset tidak ditemukan');
}
debugPrint('[DEBUG] Successfully fetched asset data: ${aset.toJson()}');
// Populate form fields with the fetched data
nameController.text = aset.nama ?? '';
descriptionController.text = aset.deskripsi ?? '';
quantityController.text = (aset.kuantitas ?? 1).toString();
// Ensure the status matches one of the available options exactly
final status = aset.status?.toLowerCase() ?? 'tersedia';
if (status == 'tersedia') {
selectedStatus.value = 'Tersedia';
} else if (status == 'pemeliharaan') {
selectedStatus.value = 'Pemeliharaan';
} else {
// Default to 'Tersedia' if status is not recognized
selectedStatus.value = 'Tersedia';
}
// Handle time options and pricing
if (aset.satuanWaktuSewa != null && aset.satuanWaktuSewa!.isNotEmpty) {
// Reset time options
timeOptions.forEach((key, value) => value.value = false);
// Process each satuan waktu sewa
for (var sws in aset.satuanWaktuSewa) {
final satuan = sws['nama_satuan_waktu']?.toString().toLowerCase() ?? '';
final harga = sws['harga'] as int? ?? 0;
final maksimalWaktu = sws['maksimal_waktu'] as int? ?? 24;
if (satuan.contains('jam')) {
timeOptions['Per Jam']?.value = true;
pricePerHourController.text = harga.toString();
maxHourController.text = maksimalWaktu.toString();
} else if (satuan.contains('hari')) {
timeOptions['Per Hari']?.value = true;
pricePerDayController.text = harga.toString();
maxDayController.text = maksimalWaktu.toString();
}
}
}
// Clear existing images
selectedImages.clear();
networkImageUrls.clear();
// Get all image URLs from the model
final allImageUrls = aset.imageUrls.toList();
// If no imageUrls but has imageUrl, use that as fallback (backward compatibility)
if (allImageUrls.isEmpty && aset.imageUrl != null && aset.imageUrl!.isNotEmpty) {
allImageUrls.add(aset.imageUrl!);
}
// Add all images to the lists
for (final imageUrl in allImageUrls) {
if (imageUrl != null && imageUrl.isNotEmpty) {
try {
// For network images, we'll store the URL in networkImageUrls
// and create a dummy XFile with the URL as path for backward compatibility
final dummyFile = XFile(imageUrl);
selectedImages.add(dummyFile);
networkImageUrls.add(imageUrl);
debugPrint('Added network image: $imageUrl');
} catch (e) {
debugPrint('Error adding network image: $e');
}
}
}
debugPrint('Total ${networkImageUrls.length} images loaded for asset $assetId');
debugPrint('[DEBUG] Successfully loaded asset data for ID: $assetId');
} catch (e, stackTrace) {
debugPrint('[ERROR] Failed to load asset data: $e');
debugPrint('Stack trace: $stackTrace');
Get.snackbar(
'Error',
'Gagal memuat data aset: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
// Optionally navigate back if there's an error
Future.delayed(const Duration(seconds: 2), () => Get.back());
} finally {
isLoading.value = false;
}
}
// Form controllers // Form controllers
final nameController = TextEditingController(); final nameController = TextEditingController();
final descriptionController = TextEditingController(); final descriptionController = TextEditingController();
@ -23,27 +203,17 @@ 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() {
@ -85,21 +255,144 @@ class PetugasTambahAsetController extends GetxController {
if (!anySelected) { if (!anySelected) {
timeOptions[option]?.value = true; timeOptions[option]?.value = true;
} }
validateForm(); validateForm();
} }
// Add image to the list (in a real app, this would handle file upload)
void addImage(String imagePath) { // Create a new asset in Supabase
selectedImages.add(imagePath); Future<String?> _createAsset(
validateForm(); Map<String, dynamic> assetData,
List<Map<String, dynamic>> satuanWaktuSewa,
) async {
try {
// Create the asset in the 'aset' table
final response = await _asetProvider.createAset(assetData);
if (response == null || response['id'] == null) {
debugPrint('❌ Failed to create asset: No response or ID from server');
return null;
}
final String assetId = response['id'].toString();
debugPrint('✅ Asset created with ID: $assetId');
// Add satuan waktu sewa
for (var sws in satuanWaktuSewa) {
final success = await _asetProvider.addSatuanWaktuSewa(
asetId: assetId,
satuanWaktu: sws['satuan_waktu'],
harga: sws['harga'],
maksimalWaktu: sws['maksimal_waktu'],
);
if (!success) {
debugPrint('❌ Failed to add satuan waktu sewa: $sws');
}
}
return assetId;
} catch (e, stackTrace) {
debugPrint('❌ Error creating asset: $e');
debugPrint('Stack trace: $stackTrace');
rethrow;
}
} }
// Remove image from the list // Update an existing asset in Supabase
Future<bool> _updateAsset(
String assetId,
Map<String, dynamic> assetData,
List<Map<String, dynamic>> satuanWaktuSewa,
) async {
try {
debugPrint('\n🔄 Starting update for asset ID: $assetId');
// 1. Extract and remove foto_aset from assetData as it's not in the aset table
final fotoAsetUrl = assetData['foto_aset'];
assetData.remove('foto_aset');
debugPrint('📝 Asset data prepared for update (without foto_aset)');
// 2. Update the main asset data (without foto_aset)
debugPrint('🔄 Updating main asset data...');
final success = await _asetProvider.updateAset(assetId, assetData);
if (!success) {
debugPrint('❌ Failed to update asset with ID: $assetId');
return false;
}
debugPrint('✅ Successfully updated main asset data');
// 3. Update satuan waktu sewa
debugPrint('\n🔄 Updating rental time units...');
// First, delete existing satuan waktu sewa
await _asetProvider.deleteSatuanWaktuSewaByAsetId(assetId);
// Then add the new ones
for (var sws in satuanWaktuSewa) {
debugPrint(' - Adding: ${sws['satuan_waktu']} (${sws['harga']} IDR)');
await _asetProvider.addSatuanWaktuSewa(
asetId: assetId,
satuanWaktu: sws['satuan_waktu'],
harga: sws['harga'] as int,
maksimalWaktu: sws['maksimal_waktu'] as int,
);
}
debugPrint('✅ Successfully updated rental time units');
// 4. Update photos in the foto_aset table if any exist
if (selectedImages.isNotEmpty || networkImageUrls.isNotEmpty) {
// Combine network URLs and local file paths
final List<String> allImageUrls = [
...networkImageUrls,
...selectedImages.map((file) => file.path),
];
debugPrint('\n🖼️ Processing photos for asset $assetId');
debugPrint(' - Network URLs: ${networkImageUrls.length}');
debugPrint(' - Local files: ${selectedImages.length}');
debugPrint(' - Total unique photos: ${allImageUrls.toSet().length} (before deduplication)');
try {
// Use updateFotoAset which handles both uploading new photos and updating the database
final photoSuccess = await _asetProvider.updateFotoAset(
asetId: assetId,
fotoUrls: allImageUrls,
);
if (!photoSuccess) {
debugPrint('⚠️ Some photos might not have been updated for asset $assetId');
// We don't fail the whole update if photo update fails
// as the main asset data has been saved successfully
} else {
debugPrint('✅ Successfully updated photos for asset $assetId');
}
} catch (e, stackTrace) {
debugPrint('❌ Error updating photos: $e');
debugPrint('Stack trace: $stackTrace');
// Continue with the update even if photo update fails
}
} else {
debugPrint(' No photos to update');
}
debugPrint('\n✅ Asset update completed successfully for ID: $assetId');
return true;
} catch (e, stackTrace) {
debugPrint('❌ Error updating asset: $e');
debugPrint('Stack trace: $stackTrace');
rethrow;
}
}
// Remove an image from the selected images list
void removeImage(int index) { 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 +426,130 @@ 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 = [];
if (timeOptions['Per Jam']?.value == true) {
final hargaPerJam = int.tryParse(pricePerHourController.text) ?? 0;
final maxJam = int.tryParse(maxHourController.text) ?? 24;
if (hargaPerJam <= 0) {
throw Exception('Harga per jam harus lebih dari 0');
}
satuanWaktuSewa.add({
'satuan_waktu': 'jam',
'harga': hargaPerJam,
'maksimal_waktu': maxJam,
});
}
if (timeOptions['Per Hari']?.value == true) {
final hargaPerHari = int.tryParse(pricePerDayController.text) ?? 0;
final maxHari = int.tryParse(maxDayController.text) ?? 30;
if (hargaPerHari <= 0) {
throw Exception('Harga per hari harus lebih dari 0');
}
satuanWaktuSewa.add({
'satuan_waktu': 'hari',
'harga': hargaPerHari,
'maksimal_waktu': maxHari,
});
}
// Return to the asset list page // Validate that at least one time option is selected
Get.back(); if (satuanWaktuSewa.isEmpty) {
throw Exception('Pilih setidaknya satu opsi waktu sewa (jam/hari)');
}
// Show success message // Handle image uploads
Get.snackbar( List<String> imageUrls = [];
'Berhasil',
'Aset berhasil ditambahkan', if (networkImageUrls.isNotEmpty) {
backgroundColor: Colors.green, // Use existing network URLs
colorText: Colors.white, imageUrls = List.from(networkImageUrls);
snackPosition: SnackPosition.BOTTOM, } else if (selectedImages.isNotEmpty) {
); // For local files, we'll upload them to Supabase Storage
// Store the file paths for now, they'll be uploaded in the provider
imageUrls = selectedImages.map((file) => file.path).toList();
debugPrint('Found ${imageUrls.length} local images to upload');
} else if (!isEditing.value) {
// For new assets, require at least one image
throw Exception('Harap unggah setidaknya satu gambar');
}
// Ensure at least one image is provided for new assets
if (imageUrls.isEmpty && !isEditing.value) {
throw Exception('Harap unggah setidaknya satu gambar');
}
// Create or update the asset
bool success;
String? createdAssetId;
if (isEditing.value && (assetId?.isNotEmpty ?? false)) {
// Update existing asset
debugPrint('🔄 Updating asset with ID: $assetId');
success = await _updateAsset(assetId!, assetData, satuanWaktuSewa);
// Update all photos if we have any
if (success && imageUrls.isNotEmpty) {
await _asetProvider.updateFotoAset(
asetId: assetId!,
fotoUrls: imageUrls,
);
}
} else {
// Create new asset
debugPrint('🔄 Creating new asset');
createdAssetId = await _createAsset(assetData, satuanWaktuSewa);
success = createdAssetId != null;
// Add all photos for new asset
if (success && createdAssetId != null && imageUrls.isNotEmpty) {
await _asetProvider.updateFotoAset(
asetId: createdAssetId,
fotoUrls: imageUrls,
);
}
}
if (success) {
// Show success message
Get.snackbar(
'Sukses',
isEditing.value ? 'Aset berhasil diperbarui' : 'Aset berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
// Navigate back with success after a short delay
await Future.delayed(const Duration(seconds: 1));
Get.back(result: true);
} else {
throw Exception('Gagal menyimpan aset');
}
} catch (e) { } catch (e) {
// Show error message // Show error message
Get.snackbar( Get.snackbar(
@ -203,8 +564,68 @@ class PetugasTambahAsetController extends GetxController {
} }
} }
// Example method to upload images (to be implemented with your backend)
// Future<List<String>> _uploadImages(List<XFile> images) async {
// List<String> urls = [];
// for (var image in images) {
// // Upload image to your server and get the URL
// // final url = await yourApiService.uploadImage(File(image.path));
// // urls.add(url);
// urls.add('https://example.com/path/to/uploaded/image.jpg'); // Mock URL
// }
// return urls;
// }
// Pick image from camera
Future<void> pickImageFromCamera() async {
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.camera,
imageQuality: 80,
maxWidth: 1024,
maxHeight: 1024,
);
if (image != null) {
selectedImages.add(image);
}
} catch (e) {
Get.snackbar(
'Error',
'Gagal mengambil gambar dari kamera: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
// Pick image from gallery
Future<void> pickImageFromGallery() async {
try {
final List<XFile>? images = await _picker.pickMultiImage(
imageQuality: 80,
maxWidth: 1024,
maxHeight: 1024,
);
if (images != null && images.isNotEmpty) {
selectedImages.addAll(images);
}
} catch (e) {
Get.snackbar(
'Error',
'Gagal memilih gambar dari galeri: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
// For demonstration purposes: add sample image // 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,11 @@
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:uuid/uuid.dart';
class PetugasTambahPaketController extends GetxController { class PetugasTambahPaketController extends GetxController {
// Form controllers // Form controllers
@ -10,14 +16,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 +37,85 @@ 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;
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 dari arguments
isEditing.value =
Get.arguments != null && Get.arguments['isEditing'] == true;
if (isEditing.value) {
final paketArg = Get.arguments['paket'];
String? paketId;
if (paketArg != null) {
if (paketArg is Map && paketArg['id'] != null) {
paketId = paketArg['id'].toString();
} else if (paketArg is PaketModel && paketArg.id != null) {
paketId = paketArg.id.toString();
}
}
if (paketId != null) {
fetchPaketDetail(paketId);
}
}
// Listen to field changes for validation // 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 +131,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 +142,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 +164,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 +215,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;
} }
} }
@ -204,6 +290,8 @@ class PetugasTambahPaketController extends GetxController {
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
checkFormChanged();
} }
// Update an existing package item // Update an existing package item
@ -301,11 +389,16 @@ class PetugasTambahPaketController extends GetxController {
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) {
packageItems.removeAt(index); if (index >= 0 && index < packageItems.length) {
packageItems.removeAt(index);
checkFormChanged();
}
Get.snackbar( Get.snackbar(
'Dihapus', 'Dihapus',
'Item berhasil dihapus dari paket', 'Item berhasil dihapus dari paket',
@ -319,10 +412,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,39 +427,204 @@ 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
'nama': nameController.text, .from('paket')
'deskripsi': descriptionController.text, .update({
'kategori': selectedCategory.value, 'nama': nameController.text,
'status': selectedStatus.value == 'Aktif', 'deskripsi': descriptionController.text,
'harga': int.parse(priceController.text), 'status': selectedStatus.value.toLowerCase(),
'gambar': selectedImages, })
'items': packageItems, .eq('id', paketId);
};
// 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
Get.snackbar( // a. Ambil foto lama dari DB
'Berhasil', final oldPhotos = await supabase
'Paket berhasil ditambahkan', .from('foto_aset')
backgroundColor: Colors.green, .select('foto_aset')
colorText: Colors.white, .eq('id_paket', paketId);
snackPosition: SnackPosition.BOTTOM, final oldPhotoUrls =
); oldPhotos
.map((e) => e['foto_aset']?.toString())
.whereType<String>()
.toSet();
final newPhotoUrls =
selectedImages
.map((img) => img is String ? img : (img.path ?? ''))
.where((e) => e.isNotEmpty)
.toSet();
// b. Hapus foto yang dihapus user (dari DB dan storage)
final removedPhotos = oldPhotoUrls.difference(newPhotoUrls);
for (final url in removedPhotos) {
await supabase
.from('foto_aset')
.delete()
.eq('foto_aset', url)
.eq('id_paket', paketId);
await _asetProvider.deleteFileFromStorage(url);
}
// c. Tambah foto baru (upload jika perlu, insert ke DB)
for (final img in selectedImages) {
String url = '';
if (img is String && img.startsWith('http')) {
url = img;
} else if (img is XFile) {
final uploaded = await _asetProvider.uploadFileToStorage(
File(img.path),
);
if (uploaded != null) url = uploaded;
}
if (url.isNotEmpty && !oldPhotoUrls.contains(url)) {
await supabase.from('foto_aset').insert({
'id_paket': paketId,
'foto_aset': url,
});
}
}
// Sukses
Get.back();
Get.snackbar(
'Berhasil',
'Paket berhasil diperbarui',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
} else {
// --- ADD LOGIC ---
final uuid = Uuid();
final String paketId = uuid.v4();
// 1. Insert ke tabel paket
await supabase.from('paket').insert({
'id': paketId,
'nama': nameController.text,
'deskripsi': descriptionController.text,
'status': selectedStatus.value.toLowerCase(),
});
// 2. Insert ke paket_item
for (var item in packageItems) {
await supabase.from('paket_item').insert({
'paket_id': paketId,
'aset_id': item['asetId'],
'kuantitas': item['jumlah'],
});
}
// 3. Insert ke satuan_waktu_sewa (ambil UUID satuan waktu)
final satuanWaktuList = await supabase
.from('satuan_waktu')
.select('id, nama_satuan_waktu');
String? jamId;
String? hariId;
for (var sw in satuanWaktuList) {
final nama = (sw['nama_satuan_waktu'] ?? '').toString().toLowerCase();
if (nama.contains('jam')) jamId = sw['id'];
if (nama.contains('hari')) hariId = sw['id'];
}
if (timeOptions['Per Jam']?.value == true && jamId != null) {
await supabase.from('satuan_waktu_sewa').insert({
'paket_id': paketId,
'satuan_waktu_id': jamId,
'harga': int.tryParse(pricePerHourController.text) ?? 0,
'maksimal_waktu': int.tryParse(maxHourController.text) ?? 0,
});
}
if (timeOptions['Per Hari']?.value == true && hariId != null) {
await supabase.from('satuan_waktu_sewa').insert({
'paket_id': paketId,
'satuan_waktu_id': hariId,
'harga': int.tryParse(pricePerDayController.text) ?? 0,
'maksimal_waktu': int.tryParse(maxDayController.text) ?? 0,
});
}
// 4. Insert ke foto_aset (upload jika perlu)
for (final img in selectedImages) {
String url = '';
if (img is String && img.startsWith('http')) {
url = img;
} else if (img is XFile) {
final uploaded = await _asetProvider.uploadFileToStorage(
File(img.path),
);
if (uploaded != null) url = uploaded;
}
if (url.isNotEmpty) {
await supabase.from('foto_aset').insert({
'id_paket': paketId,
'foto_aset': url,
});
}
}
// Sukses
Get.back();
Get.snackbar(
'Berhasil',
'Paket berhasil ditambahkan',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
}
} catch (e) { } 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.BOTTOM,
@ -390,4 +645,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.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
Future<void> pickImageFromGallery() async {
try {
final List<XFile>? images = await _picker.pickMultiImage(
imageQuality: 80,
maxWidth: 1024,
maxHeight: 1024,
);
if (images != null && images.isNotEmpty) {
for (final img in images) {
selectedImages.add(img);
}
}
} catch (e) {
Get.snackbar(
'Error',
'Gagal memilih gambar dari galeri: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
void checkFormChanged() {
final current = {
'nama': nameController.text,
'deskripsi': descriptionController.text,
'status': selectedStatus.value,
'images': List.from(selectedImages),
'items': List.from(packageItems),
'perJam': timeOptions['Per Jam']?.value ?? false,
'perHari': timeOptions['Per Hari']?.value ?? false,
'hargaJam': pricePerHourController.text,
'maxJam': maxHourController.text,
'hargaHari': pricePerDayController.text,
'maxHari': maxDayController.text,
};
isFormChanged.value = current.toString() != initialFormData.toString();
}
} }

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,13 @@ 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
Get.toNamed(
Routes.PETUGAS_TAMBAH_ASET,
arguments: {'isEditing': false, 'assetData': null},
);
},
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 +137,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 +207,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) {
final aset = controller.filteredAsetList[index]; if (index < controller.filteredAsetList.length) {
return _buildAssetCard(context, aset); final aset = controller.filteredAsetList[index];
return _buildAssetCard(context, aset);
} else {
// Blank space at the end
return const SizedBox(height: 80);
}
}, },
), ),
); );
@ -266,7 +223,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),
@ -290,21 +271,46 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
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(
child: Center( imageUrl: imageUrl,
child: Icon( fit: BoxFit.cover,
_getAssetIcon(aset['kategori']), placeholder:
color: AppColorsPetugas.navyBlue, (context, url) => Container(
size: 32, color: AppColorsPetugas.babyBlueLight,
child: Center(
child: Icon(
_getAssetIcon(
kategori,
), // Show category icon as placeholder
color: AppColorsPetugas.navyBlue.withOpacity(
0.5,
),
size: 32,
),
),
),
errorWidget:
(context, url, error) => Container(
color: AppColorsPetugas.babyBlueLight,
child: Center(
child: Icon(
Icons
.broken_image, // Or your preferred error icon
color: AppColorsPetugas.navyBlue.withOpacity(
0.5,
),
size: 32,
),
),
),
), ),
), ),
), ),
@ -323,8 +329,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 +339,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(
style: TextStyle( builder: (context) {
fontSize: 12, final satuanWaktuList =
color: AppColorsPetugas.textSecondary, (aset['satuanWaktuSewa'] is List)
), ? List<Map<String, dynamic>>.from(
aset['satuanWaktuSewa'],
)
: [];
final validSatuanWaktu =
satuanWaktuList
.where(
(sw) =>
(sw['harga'] ?? 0) > 0 &&
(sw['nama_satuan_waktu'] !=
null &&
(sw['nama_satuan_waktu']
as String)
.isNotEmpty),
)
.toList();
if (validSatuanWaktu.isNotEmpty) {
return Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children:
validSatuanWaktu.map((sw) {
final harga = sw['harga'] ?? 0;
final satuan =
sw['nama_satuan_waktu'] ?? '';
return Text(
'${controller.formatPrice(harga)} / $satuan',
style: TextStyle(
fontSize: 12,
color:
AppColorsPetugas
.textSecondary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
}).toList(),
);
} else {
// fallback: harga tunggal
return Text(
'${controller.formatPrice(aset['harga'] ?? 0)} / ${aset['satuan_waktu']?.toString().capitalizeFirst ?? 'Hari'}',
style: TextStyle(
fontSize: 12,
color: AppColorsPetugas.textSecondary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
}
},
), ),
], ],
), ),
@ -383,11 +440,36 @@ 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.BOTTOM,
);
return;
}
Get.toNamed(
Routes.PETUGAS_TAMBAH_ASET,
arguments: {
'isEditing': true,
'assetId': assetId,
},
);
},
child: Container( child: Container(
padding: const EdgeInsets.all(5), padding: const EdgeInsets.all(5),
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@ -5,6 +5,7 @@ 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';
class PetugasBumdesDashboardView class PetugasBumdesDashboardView
extends GetView<PetugasBumdesDashboardController> { extends GetView<PetugasBumdesDashboardController> {
@ -23,12 +24,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,
@ -118,8 +114,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,25 +150,51 @@ class PetugasBumdesDashboardView
children: [ children: [
Row( Row(
children: [ children: [
Container( Obx(() {
padding: const EdgeInsets.all(12), final avatar = controller.avatarUrl.value;
decoration: BoxDecoration( if (avatar.isNotEmpty) {
color: Colors.white.withOpacity(0.2), return ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
boxShadow: [ child: Image.network(
BoxShadow( avatar,
color: Colors.black.withOpacity(0.1), width: 48,
blurRadius: 8, height: 48,
offset: const Offset(0, 3), 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 {
child: const Icon( return Container(
Icons.person, padding: const EdgeInsets.all(12),
color: Colors.white, decoration: BoxDecoration(
size: 30, color: Colors.white.withOpacity(0.2),
), borderRadius: BorderRadius.circular(12),
), boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: const Icon(
Icons.person,
color: Colors.white,
size: 30,
),
);
}
}),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Column( child: Column(
@ -208,15 +228,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 +664,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 +690,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 +703,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 +736,8 @@ class PetugasBumdesDashboardView
), ),
), ),
], ],
), );
), }),
], ],
), ),
), ),
@ -747,12 +774,29 @@ class PetugasBumdesDashboardView
return Row( return Row(
children: [ children: [
Expanded( Expanded(
child: _buildRevenueQuickInfo( child: Obx(() {
'Pendapatan Sewa', final stats = controller.pembayaranStats;
controller.pendapatanSewa.value, final totalTunai = stats['totalTunai'] ?? 0.0;
AppColorsPetugas.navyBlue, return _buildRevenueQuickInfo(
Icons.shopping_cart_outlined, 'Tunai',
), formatRupiah(totalTunai),
AppColorsPetugas.navyBlue,
Icons.payments,
);
}),
),
const SizedBox(width: 12),
Expanded(
child: Obx(() {
final stats = controller.pembayaranStats;
final totalTransfer = stats['totalTransfer'] ?? 0.0;
return _buildRevenueQuickInfo(
'Transfer',
formatRupiah(totalTransfer),
AppColorsPetugas.blueGrotto,
Icons.account_balance,
);
}),
), ),
], ],
); );
@ -811,81 +855,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 +881,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 +897,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,

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,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
), ),
), ),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET), onPressed:
() => Get.toNamed(
Routes.PETUGAS_TAMBAH_PAKET,
arguments: {'isEditing': false},
),
label: Text( label: Text(
'Tambah Paket', 'Tambah Paket',
style: TextStyle( style: TextStyle(
@ -115,7 +121,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 +142,11 @@ 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:
() => Get.toNamed(
Routes.PETUGAS_TAMBAH_PAKET,
arguments: {'isEditing': false},
),
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 +171,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) {
return _buildPaketCard(context, paket); final paket = controller.filteredPackages[index];
return _buildPaketCard(context, paket);
} else {
// Blank space at the end
return const SizedBox(height: 80);
}
}, },
), ),
); );
}); });
} }
Widget _buildPaketCard(BuildContext context, Map<String, dynamic> paket) { // 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),
@ -196,22 +380,83 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
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:
child: Center( foto != null && foto.isNotEmpty
child: Icon( ? Image.network(
_getPaketIcon(paket['kategori']), foto,
color: AppColorsPetugas.navyBlue, width: 80,
size: 32, height: 80,
), fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) => Container(
color: AppColorsPetugas.babyBlueLight,
child: Center(
child: Icon(
_getPaketIcon(
_getTimeUnitName(
isPaketModel
? (paket
.satuanWaktuSewa
.isNotEmpty
? paket
.satuanWaktuSewa
.first['satuan_waktu_id'] ??
'hari'
: 'hari')
: (paket['satuanWaktuSewa'] !=
null &&
paket['satuanWaktuSewa']
.isNotEmpty
? paket['satuanWaktuSewa'][0]['satuan_waktu_id']
?.toString() ??
'hari'
: 'hari'),
),
),
color: AppColorsPetugas.navyBlue
.withOpacity(0.5),
size: 32,
),
),
),
)
: Container(
color: AppColorsPetugas.babyBlueLight,
child: Center(
child: Icon(
_getPaketIcon(
_getTimeUnitName(
isPaketModel
? (paket.satuanWaktuSewa.isNotEmpty
? paket
.satuanWaktuSewa
.first['satuan_waktu_id'] ??
'hari'
: 'hari')
: (paket['satuanWaktuSewa'] != null &&
paket['satuanWaktuSewa']
.isNotEmpty
? paket['satuanWaktuSewa'][0]['satuan_waktu_id']
?.toString() ??
'hari'
: 'hari'),
),
),
color: AppColorsPetugas.navyBlue.withOpacity(
0.5,
),
size: 32,
),
),
),
), ),
), ),
@ -228,9 +473,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,13 +485,119 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( // Prices with time units
'Rp ${_formatPrice(paket['harga'])}', Builder(
style: TextStyle( builder: (context) {
fontSize: 12, final List<Map<String, dynamic>> timeUnits =
color: AppColorsPetugas.textSecondary, [];
),
// Get all time units
if (isPaketModel &&
paket.satuanWaktuSewa.isNotEmpty) {
timeUnits.addAll(paket.satuanWaktuSewa);
} else if (!isPaketModel &&
paket['satuanWaktuSewa'] != null &&
paket['satuanWaktuSewa'].isNotEmpty) {
timeUnits.addAll(
List<Map<String, dynamic>>.from(
paket['satuanWaktuSewa'],
),
);
}
// If no time units, show nothing
if (timeUnits.isEmpty)
return const SizedBox.shrink();
// Filter out time units with price 0 or null
final validTimeUnits =
timeUnits.where((unit) {
final price =
unit['harga'] is int
? unit['harga']
: int.tryParse(
unit['harga']
?.toString() ??
'0',
) ??
0;
return price > 0;
}).toList();
if (validTimeUnits.isEmpty)
return const SizedBox.shrink();
return Column(
children:
validTimeUnits
.asMap()
.entries
.map((entry) {
final index = entry.key;
final unit = entry.value;
final unitPrice =
unit['harga'] is int
? unit['harga']
: int.tryParse(
unit['harga']
?.toString() ??
'0',
) ??
0;
final unitName = _getTimeUnitName(
unit['satuan_waktu_id'],
);
final isFirst = index == 0;
if (unitPrice <= 0)
return const SizedBox.shrink();
return Row(
children: [
Flexible(
child: Text(
'Rp ${_formatPrice(unitPrice)}/$unitName',
style: TextStyle(
fontSize: 12,
color:
AppColorsPetugas
.textSecondary,
),
maxLines: 2,
overflow:
TextOverflow.ellipsis,
softWrap: true,
),
),
],
);
})
.where(
(widget) => widget is! SizedBox,
)
.toList(),
);
},
), ),
if (!isPaketModel &&
paket['harga'] != null &&
(paket['harga'] is int
? paket['harga']
: int.tryParse(
paket['harga']?.toString() ??
'0',
) ??
0) >
0) ...[
const SizedBox(height: 4),
Text(
'Rp ${_formatPrice(paket['harga'])}',
style: TextStyle(
fontSize: 12,
color: AppColorsPetugas.textSecondary,
),
),
],
], ],
), ),
), ),
@ -258,25 +610,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 +648,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 +711,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,7 +796,27 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
); );
} }
void _showPaketDetails(BuildContext context, Map<String, dynamic> paket) { void _showPaketDetails(BuildContext context, dynamic paket) {
// Handle both Map and PaketModel for backward compatibility
final isPaketModel = paket is PaketModel;
final String nama =
isPaketModel
? paket.nama
: (paket['nama']?.toString() ?? 'Paket Tanpa Nama');
final String? deskripsi =
isPaketModel ? paket.deskripsi : paket['deskripsi']?.toString();
final bool isAvailable =
isPaketModel
? (paket.kuantitas > 0)
: ((paket['kuantitas'] as int?) ?? 0) > 0;
final dynamic harga =
isPaketModel
? (paket.satuanWaktuSewa.isNotEmpty
? paket.satuanWaktuSewa.first['harga']
: paket.harga)
: (paket['harga'] ?? 0);
// Items are not part of the PaketModel, so we'll use an empty list
final List<Map<String, dynamic>> items = [];
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
@ -448,7 +838,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
paket['nama'], nama,
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -473,16 +863,15 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildDetailItem('Kategori', paket['kategori']),
_buildDetailItem( _buildDetailItem(
'Harga', 'Harga',
controller.formatPrice(paket['harga']), 'Rp ${_formatPrice(harga)}',
), ),
_buildDetailItem( _buildDetailItem(
'Status', 'Status',
paket['tersedia'] ? 'Tersedia' : 'Tidak Tersedia', isAvailable ? 'Tersedia' : 'Tidak Tersedia',
), ),
_buildDetailItem('Deskripsi', paket['deskripsi']), _buildDetailItem('Deskripsi', deskripsi ?? '-'),
], ],
), ),
), ),
@ -502,11 +891,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
child: ListView.separated( child: ListView.separated(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
itemCount: paket['items'].length, itemCount: items.length,
separatorBuilder: separatorBuilder:
(context, index) => const Divider(height: 1), (context, index) => const Divider(height: 1),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = paket['items'][index]; final item = items[index];
return ListTile( return ListTile(
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: AppColorsPetugas.babyBlue, backgroundColor: AppColorsPetugas.babyBlue,
@ -601,10 +990,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
); );
} }
void _showAddEditPaketDialog( void _showAddEditPaketDialog(BuildContext context, {dynamic paket}) {
BuildContext context, { // Handle both Map and PaketModel for backward compatibility
Map<String, dynamic>? paket, final isPaketModel = paket is PaketModel;
}) { final String? id = isPaketModel ? paket.id : paket?['id'];
final String title = id == null ? 'Tambah Paket' : 'Edit Paket';
final isEditing = paket != null; final isEditing = paket != null;
// This would be implemented with proper form validation in a real app // This would be implemented with proper form validation in a real app
@ -613,7 +1003,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text( title: Text(
isEditing ? 'Edit Paket' : 'Tambah Paket Baru', title,
style: TextStyle(color: AppColorsPetugas.navyBlue), style: TextStyle(color: AppColorsPetugas.navyBlue),
), ),
content: const Text( content: const Text(
@ -652,10 +1042,13 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
); );
} }
void _showDeleteConfirmation( void _showDeleteConfirmation(BuildContext context, dynamic paket) {
BuildContext context, // Handle both Map and PaketModel for backward compatibility
Map<String, dynamic> paket, 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( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
@ -664,9 +1057,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
'Konfirmasi Hapus', 'Konfirmasi Hapus',
style: TextStyle(color: AppColorsPetugas.navyBlue), style: TextStyle(color: AppColorsPetugas.navyBlue),
), ),
content: Text( content: Text('Apakah Anda yakin ingin menghapus paket "$nama"?'),
'Apakah Anda yakin ingin menghapus paket "${paket['nama']}"?',
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
@ -678,7 +1069,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
controller.deletePaket(paket['id']); controller.deletePaket(id);
Get.snackbar( Get.snackbar(
'Paket Dihapus', 'Paket Dihapus',
'Paket berhasil dihapus dari sistem', 'Paket berhasil dihapus dari sistem',

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,10 +209,21 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
), ),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
isDense: true, isDense: true,
suffixIcon: Icon( suffixIcon: Obx(
Icons.tune_rounded, () =>
color: AppColorsPetugas.textSecondary, controller.searchQuery.value.isNotEmpty
size: 20, ? IconButton(
icon: Icon(
Icons.close,
color: AppColorsPetugas.textSecondary,
size: 20,
),
onPressed: () {
searchController.clear();
controller.setSearchQuery('');
},
)
: SizedBox.shrink(),
), ),
), ),
), ),
@ -241,17 +257,44 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
final filteredList = 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 &&
style: TextStyle( sewa.wargaAvatar.isNotEmpty)
fontSize: 18, ? NetworkImage(sewa.wargaAvatar)
fontWeight: FontWeight.bold, : null,
color: AppColorsPetugas.blueGrotto, child:
), (sewa.wargaAvatar == null || sewa.wargaAvatar.isEmpty)
), ? Text(
sewa.wargaNama.substring(0, 1).toUpperCase(),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.blueGrotto,
),
)
: null,
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
@ -395,55 +460,22 @@ 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), Text(
Row( 'Tanggal Pesan: ' +
children: [ (sewa.tanggalPemesanan != null
Container( ? '${sewa.tanggalPemesanan.day.toString().padLeft(2, '0')}-${sewa.tanggalPemesanan.month.toString().padLeft(2, '0')}-${sewa.tanggalPemesanan.year}'
padding: const EdgeInsets.symmetric( : '-'),
horizontal: 8, style: TextStyle(
vertical: 3, fontSize: 12,
), color: AppColorsPetugas.textSecondary,
decoration: BoxDecoration( ),
color: statusColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(30),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
statusIcon,
size: 12,
color: statusColor,
),
const SizedBox(width: 4),
Text(
status,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: statusColor,
),
),
],
),
),
const SizedBox(width: 8),
Text(
'#${sewa['order_id']}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColorsPetugas.textSecondary,
),
),
],
), ),
], ],
), ),
@ -460,7 +492,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,33 +513,51 @@ 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
Container( if (fotoAsetAtauPaket != null &&
padding: const EdgeInsets.all(8), fotoAsetAtauPaket.isNotEmpty)
decoration: BoxDecoration( ClipRRect(
color: AppColorsPetugas.babyBlueLight,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: Image.network(
fotoAsetAtauPaket,
width: 40,
height: 40,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) => Icon(
Icons.inventory_2_outlined,
size: 28,
color: AppColorsPetugas.blueGrotto,
),
),
)
else
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueLight,
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.inventory_2_outlined,
size: 20,
color: AppColorsPetugas.blueGrotto,
),
), ),
child: Icon(
Icons.inventory_2_outlined,
size: 20,
color: AppColorsPetugas.blueGrotto,
),
),
const SizedBox(width: 12), 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 +574,7 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'${sewa['tanggal_mulai']} - ${sewa['tanggal_selesai']}', '${sewa.waktuMulai.toIso8601String().substring(0, 10)} - ${sewa.waktuSelesai.toIso8601String().substring(0, 10)}',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: AppColorsPetugas.textSecondary, color: AppColorsPetugas.textSecondary,

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(
backgroundColor: Colors.white, onTap: () => FocusScope.of(context).unfocus(),
appBar: AppBar( child: Obx(() => Scaffold(
title: const Text( backgroundColor: Colors.white,
'Tambah Aset', appBar: AppBar(
style: TextStyle(fontWeight: FontWeight.w600), title: Text(
), controller.isEditing.value ? 'Edit Aset' : 'Tambah Aset',
backgroundColor: AppColorsPetugas.navyBlue, style: const TextStyle(fontWeight: FontWeight.w600),
elevation: 0,
centerTitle: true,
),
body: SafeArea(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [_buildHeaderSection(), _buildFormSection(context)],
), ),
backgroundColor: AppColorsPetugas.navyBlue,
elevation: 0,
centerTitle: true,
), ),
), body: Stack(
bottomNavigationBar: _buildBottomBar(), children: [
SafeArea(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderSection(),
_buildFormSection(context),
],
),
),
),
if (controller.isLoading.value)
Container(
color: Colors.black54,
child: const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(AppColorsPetugas.blueGrotto),
),
),
),
],
),
bottomNavigationBar: _buildBottomBar(),
)),
); );
} }
Widget _buildHeaderSection() { 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,69 +109,36 @@ 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: [ title: 'Status',
Expanded( options: controller.statusOptions,
child: _buildCategorySelect( selectedOption: controller.selectedStatus,
title: 'Kategori', onChanged: controller.setStatus,
options: controller.categoryOptions, icon: Icons.check_circle,
selectedOption: controller.selectedCategory,
onChanged: controller.setCategory,
icon: Icons.inventory_2,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildCategorySelect(
title: 'Status',
options: controller.statusOptions,
selectedOption: controller.selectedStatus,
onChanged: controller.setStatus,
icon: Icons.check_circle,
),
),
],
), ),
const SizedBox(height: 24), 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: [ label: 'Kuantitas',
Expanded( hint: 'Jumlah aset',
flex: 2, controller: controller.quantityController,
child: _buildTextField( isRequired: true,
label: 'Kuantitas', keyboardType: TextInputType.number,
hint: 'Jumlah aset', inputFormatters: [FilteringTextInputFormatter.digitsOnly],
controller: controller.quantityController, prefixIcon: Icons.numbers,
isRequired: true,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
prefixIcon: Icons.numbers,
),
),
const SizedBox(width: 12),
Expanded(
flex: 3,
child: _buildTextField(
label: 'Satuan Ukur',
hint: 'contoh: Unit, Buah',
controller: controller.unitOfMeasureController,
prefixIcon: Icons.straighten,
),
),
],
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@ -654,6 +599,114 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
); );
} }
// Show image source options
void _showImageSourceOptions() {
Get.bottomSheet(
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
Text(
'Pilih Sumber Gambar',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.navyBlue,
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildImageSourceOption(
icon: Icons.camera_alt,
label: 'Kamera',
onTap: () {
Get.back();
controller.pickImageFromCamera();
},
),
_buildImageSourceOption(
icon: Icons.photo_library,
label: 'Galeri',
onTap: () {
Get.back();
controller.pickImageFromGallery();
},
),
],
),
const SizedBox(height: 10),
TextButton(
onPressed: () => Get.back(),
child: const Text('Batal'),
),
],
),
),
isScrollControlled: true,
);
}
Widget _buildImageSourceOption({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Column(
children: [
Container(
width: 70,
height: 70,
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueBright,
shape: BoxShape.circle,
),
child: Icon(
icon,
size: 30,
color: AppColorsPetugas.blueGrotto,
),
),
const SizedBox(height: 8),
Text(
label,
style: TextStyle(
fontSize: 14,
color: AppColorsPetugas.textPrimary,
),
),
],
),
);
}
Widget _buildImageUploader() { 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,69 +785,107 @@ 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(
width: 100, children: [
height: 100, Container(
decoration: BoxDecoration( width: 100,
color: AppColorsPetugas.babyBlueLight, height: 100,
borderRadius: BorderRadius.circular(10), decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey[300]!),
),
child: Obx(
() {
// Check if we have a network URL for this index
if (index < controller.networkImageUrls.length &&
controller.networkImageUrls[index].isNotEmpty) {
// Display network image
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
controller.networkImageUrls[index],
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
errorBuilder: (context, error, stackTrace) {
return const Center(
child: Icon(Icons.error_outline, color: Colors.red),
);
},
),
);
} else {
// Display local file
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: FutureBuilder<File>(
future: File(controller.selectedImages[index].path).exists().then((exists) {
if (exists) {
return File(controller.selectedImages[index].path);
} else {
return File(controller.selectedImages[index].path);
}
}),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
return Image.file(
snapshot.data!,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[200],
child: const Icon(Icons.broken_image, color: Colors.grey),
);
},
);
} else {
return Container(
color: Colors.grey[200],
child: const Center(
child: CircularProgressIndicator(),
),
);
}
},
),
);
}
},
),
),
Positioned(
top: 4,
right: 4,
child: GestureDetector(
onTap: () => controller.removeImage(index),
child: Container( child: Container(
width: 100, padding: const EdgeInsets.all(2),
height: 100, decoration: const BoxDecoration(
color: AppColorsPetugas.babyBlueLight, color: Colors.white,
child: Center( shape: BoxShape.circle,
child: Icon( boxShadow: [
Icons.image, BoxShadow(
color: AppColorsPetugas.blueGrotto, color: Colors.black26,
size: 40, blurRadius: 4,
), offset: Offset(0, 1),
),
],
),
child: const Icon(
Icons.close,
size: 16,
color: Colors.red,
), ),
), ),
), ),
Positioned( ),
top: 4, ],
right: 4, ),
child: GestureDetector( ).toList(),
onTap: () => controller.removeImage(index),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Icon(
Icons.close,
color: AppColorsPetugas.error,
size: 16,
),
),
),
),
],
),
);
}),
], ],
), ),
), ),
@ -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,11 @@ 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.isEditing.value ? 'Edit Paket' : 'Tambah Paket',
style: const TextStyle(fontWeight: FontWeight.w600),
),
), ),
backgroundColor: AppColorsPetugas.navyBlue, backgroundColor: AppColorsPetugas.navyBlue,
elevation: 0, elevation: 0,
@ -24,7 +27,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 +35,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 +77,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 +106,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 +113,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),
], ],
), ),
@ -310,7 +271,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 +280,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 +383,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 +392,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']})',
), ),
@ -757,7 +718,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
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,
@ -791,69 +752,82 @@ 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( ) {
width: 100, final img = controller.selectedImages[index];
height: 100, return Stack(
decoration: BoxDecoration( children: [
color: AppColorsPetugas.babyBlueLight, Container(
borderRadius: BorderRadius.circular(10), width: 100,
boxShadow: [ height: 100,
BoxShadow( decoration: BoxDecoration(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: Container( border: Border.all(color: Colors.grey[300]!),
width: 100,
height: 100,
color: AppColorsPetugas.babyBlueLight,
child: Center(
child: Icon(
Icons.image,
color: AppColorsPetugas.blueGrotto,
size: 40,
),
),
),
), ),
Positioned( child: ClipRRect(
top: 4, borderRadius: BorderRadius.circular(8),
right: 4, child:
child: GestureDetector( (img is String && img.startsWith('http'))
onTap: () => controller.removeImage(index), ? Image.network(
child: Container( img,
padding: const EdgeInsets.all(4), fit: BoxFit.cover,
decoration: BoxDecoration( width: double.infinity,
color: Colors.white, height: double.infinity,
shape: BoxShape.circle, errorBuilder:
boxShadow: [ (context, error, stackTrace) =>
BoxShadow( const Center(
color: Colors.black.withOpacity(0.1), child: Icon(
blurRadius: 3, Icons.broken_image,
offset: const Offset(0, 1), color: Colors.grey,
),
),
)
: (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,
),
),
), ),
], ),
), ),
child: Icon( Positioned(
Icons.close, top: 4,
color: AppColorsPetugas.error, right: 4,
size: 16, child: InkWell(
), onTap: () => controller.removeImage(index),
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
size: 18,
color: Colors.red,
), ),
), ),
), ),
], ),
), ],
); );
}), }),
], ],
@ -864,6 +838,104 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
); );
} }
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,
),
),
],
),
);
}
Widget _buildBottomBar() { Widget _buildBottomBar() {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
@ -899,26 +971,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,
), ),
@ -929,4 +1012,226 @@ 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.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],
decoration: InputDecoration(
hintText: 'Masukkan harga',
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
prefixText: 'Rp ',
filled: true,
fillColor: AppColorsPetugas.babyBlueBright,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
),
),
],
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
maxLabel,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColorsPetugas.textSecondary,
),
),
const SizedBox(height: 8),
TextFormField(
controller: maxController,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
hintText: 'Opsional',
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
filled: true,
fillColor: AppColorsPetugas.babyBlueBright,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
),
),
],
),
),
],
),
],
),
);
}
} }

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 '../../../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';
class PetugasSideNavbar extends StatelessWidget { class PetugasSideNavbar extends StatelessWidget {
@ -11,7 +12,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,24 +33,46 @@ 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(() {
decoration: BoxDecoration( final avatar = controller.avatarUrl.value;
shape: BoxShape.circle, if (avatar.isNotEmpty) {
border: Border.all(color: Colors.white, width: 2), return Container(
), decoration: BoxDecoration(
child: CircleAvatar( shape: BoxShape.circle,
radius: 30, border: Border.all(color: Colors.white, width: 2),
backgroundColor: Colors.white, ),
child: Icon(Icons.person, color: AppColors.primary, size: 36), child: CircleAvatar(
), radius: 30,
), backgroundColor: Colors.white,
backgroundImage: NetworkImage(avatar),
onBackgroundImageError: (error, stackTrace) {},
),
);
} else {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: CircleAvatar(
radius: 30,
backgroundColor: Colors.white,
child: Icon(
Icons.person,
color: AppColors.primary,
size: 36,
),
),
);
}
}),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Column( child: Column(

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

@ -8,17 +8,11 @@ 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);
} }
// Ensure AsetProvider is registered // Ensure AsetProvider is registered
if (!Get.isRegistered<AsetProvider>()) { if (!Get.isRegistered<AsetProvider>()) {
Get.put(AsetProvider(), permanent: true); Get.put(AsetProvider(), permanent: true);

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

View File

@ -2,11 +2,14 @@ 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 '../../../services/navigation_service.dart'; import '../../../services/navigation_service.dart';
import '../../../data/providers/aset_provider.dart';
import 'package:intl/intl.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;
@ -28,6 +31,11 @@ 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() {
super.onInit(); super.onInit();
@ -36,6 +44,7 @@ class WargaDashboardController extends GetxController {
navigationService.setNavIndex(0); navigationService.setNavIndex(0);
// Load user data // Load user data
fetchProfileFromWargaDesa();
_loadUserData(); _loadUserData();
// Load sample data // Load sample data
@ -46,6 +55,12 @@ class WargaDashboardController extends GetxController {
// Load unpaid rentals // Load unpaid rentals
loadUnpaidRentals(); loadUnpaidRentals();
// Debug count sewa_aset by status
_debugCountSewaAset();
// Load sewa aktif
loadActiveRentals();
} }
Future<void> _loadUserData() async { Future<void> _loadUserData() async {
@ -112,7 +127,7 @@ class WargaDashboardController extends GetxController {
} }
void refreshData() { void refreshData() {
// Refresh data from repository fetchProfileFromWargaDesa();
_loadSampleData(); _loadSampleData();
loadDummyData(); loadDummyData();
} }
@ -129,12 +144,17 @@ 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 toWargaSewaTabAktif() {
// Navigasi ke halaman warga sewa dan tab Aktif (index 3)
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 3});
}
void logout() async { void logout() async {
await _authProvider.signOut(); await _authProvider.signOut();
navigationService.toLogin(); navigationService.toLogin();
@ -177,4 +197,137 @@ 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) return;
final userId = user.id;
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) {
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() ?? '';
}
} catch (e) {
print('Error fetching profile from warga_desa: $e');
}
}
} }

View File

@ -12,10 +12,10 @@ class WargaSewaController extends GetxController
// Get navigation service // Get navigation service
final NavigationService navigationService = Get.find<NavigationService>(); final NavigationService navigationService = Get.find<NavigationService>();
// Get auth provider for user data and sewa_aset queries // Get auth provider for user data and sewa_aset queries
final AuthProvider authProvider = Get.find<AuthProvider>(); final AuthProvider authProvider = Get.find<AuthProvider>();
// Get aset provider for asset data // Get aset provider for asset data
final AsetProvider asetProvider = Get.find<AsetProvider>(); final AsetProvider asetProvider = Get.find<AsetProvider>();
@ -25,33 +25,35 @@ 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;
final isLoadingPending = false.obs; final isLoadingPending = false.obs;
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
@ -111,25 +137,25 @@ class WargaSewaController extends GetxController
Future<void> loadRentalsData() async { Future<void> loadRentalsData() async {
try { try {
isLoading.value = true; isLoading.value = true;
// Clear existing data // Clear existing data
rentals.clear(); rentals.clear();
// Get sewa_aset data with status "MENUNGGU PEMBAYARAN" or "PEMBAYARAN DENDA" // Get sewa_aset data with status "MENUNGGU PEMBAYARAN" or "PEMBAYARAN DENDA"
final sewaAsetList = await authProvider.getSewaAsetByStatus([ final sewaAsetList = await authProvider.getSewaAsetByStatus([
'MENUNGGU PEMBAYARAN', 'MENUNGGU PEMBAYARAN',
'PEMBAYARAN DENDA' 'PEMBAYARAN DENDA',
]); ]);
debugPrint('Fetched ${sewaAsetList.length} sewa_aset records'); debugPrint('Fetched ${sewaAsetList.length} 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 // 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) { if (sewaAset['aset_id'] != null) {
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']); final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
if (asetData != null) { if (asetData != null) {
@ -137,7 +163,7 @@ class WargaSewaController extends GetxController
imageUrl = asetData.imageUrl; imageUrl = asetData.imageUrl;
} }
} }
// Parse waktu mulai and waktu selesai // Parse waktu mulai and waktu selesai
DateTime? waktuMulai; DateTime? waktuMulai;
DateTime? waktuSelesai; DateTime? waktuSelesai;
@ -146,20 +172,21 @@ class WargaSewaController extends GetxController
String jamMulai = ''; String jamMulai = '';
String jamSelesai = ''; String jamSelesai = '';
String rentangWaktu = ''; String rentangWaktu = '';
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) { if (sewaAset['waktu_mulai'] != null &&
sewaAset['waktu_selesai'] != null) {
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']); waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']); waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
// Format for display // Format for display
final formatTanggal = DateFormat('dd-MM-yyyy'); final formatTanggal = DateFormat('dd-MM-yyyy');
final formatWaktu = DateFormat('HH:mm'); final formatWaktu = DateFormat('HH:mm');
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID'); final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
tanggalSewa = formatTanggalLengkap.format(waktuMulai); tanggalSewa = formatTanggalLengkap.format(waktuMulai);
jamMulai = formatWaktu.format(waktuMulai); jamMulai = formatWaktu.format(waktuMulai);
jamSelesai = formatWaktu.format(waktuSelesai); jamSelesai = formatWaktu.format(waktuSelesai);
// Format based on satuan waktu // Format based on satuan waktu
if (namaSatuanWaktu.toLowerCase() == 'jam') { if (namaSatuanWaktu.toLowerCase() == 'jam') {
// For hours, show time range on same day // For hours, show time range on same day
@ -173,12 +200,13 @@ class WargaSewaController extends GetxController
// Default format // Default format
rentangWaktu = '$jamMulai - $jamSelesai'; rentangWaktu = '$jamMulai - $jamSelesai';
} }
// Full time format for waktuSewa // Full time format for waktuSewa
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - ' waktuSewa =
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}'; '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
} }
// Format price // Format price
String totalPrice = 'Rp 0'; String totalPrice = 'Rp 0';
if (sewaAset['total'] != null) { if (sewaAset['total'] != null) {
@ -189,7 +217,7 @@ class WargaSewaController extends GetxController
); );
totalPrice = formatter.format(sewaAset['total']); totalPrice = formatter.format(sewaAset['total']);
} }
// Add to rentals list // Add to rentals list
rentals.add({ rentals.add({
'id': sewaAset['id'] ?? '', 'id': sewaAset['id'] ?? '',
@ -208,9 +236,10 @@ 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'],
}); });
} }
debugPrint('Processed ${rentals.length} rental records'); debugPrint('Processed ${rentals.length} rental records');
} catch (e) { } catch (e) {
debugPrint('Error loading rentals data: $e'); debugPrint('Error loading rentals data: $e');
@ -245,28 +274,67 @@ 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.BOTTOM,
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.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
} }
// Navigate to payment page with the selected rental data // Navigate to payment page with the selected rental data
void viewRentalDetail(Map<String, dynamic> rental) { void viewRentalDetail(Map<String, dynamic> rental) {
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( Get.toNamed(
Routes.PEMBAYARAN_SEWA, Routes.PEMBAYARAN_SEWA,
arguments: { arguments: {'orderId': rental['id'], 'rentalData': rental},
'orderId': rental['id'],
'rentalData': rental,
},
); );
} }
void payRental(String id) { void payRental(String id) {
Get.snackbar( Get.snackbar(
'Info', 'Info',
@ -274,27 +342,27 @@ class WargaSewaController extends GetxController
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
); );
} }
// Load data for the Selesai tab (status: SELESAI) // Load data for the Selesai tab (status: SELESAI)
Future<void> loadCompletedRentals() async { Future<void> loadCompletedRentals() async {
try { try {
isLoadingCompleted.value = true; isLoadingCompleted.value = true;
// Clear existing data // Clear existing data
completedRentals.clear(); completedRentals.clear();
// Get sewa_aset data with status "SELESAI" // Get sewa_aset data with status "SELESAI"
final sewaAsetList = await authProvider.getSewaAsetByStatus(['SELESAI']); final sewaAsetList = await authProvider.getSewaAsetByStatus(['SELESAI']);
debugPrint('Fetched ${sewaAsetList.length} completed sewa_aset records'); debugPrint('Fetched ${sewaAsetList.length} completed 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 // 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) { if (sewaAset['aset_id'] != null) {
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']); final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
if (asetData != null) { if (asetData != null) {
@ -302,7 +370,7 @@ class WargaSewaController extends GetxController
imageUrl = asetData.imageUrl; imageUrl = asetData.imageUrl;
} }
} }
// Parse waktu mulai and waktu selesai // Parse waktu mulai and waktu selesai
DateTime? waktuMulai; DateTime? waktuMulai;
DateTime? waktuSelesai; DateTime? waktuSelesai;
@ -311,20 +379,21 @@ class WargaSewaController extends GetxController
String jamMulai = ''; String jamMulai = '';
String jamSelesai = ''; String jamSelesai = '';
String rentangWaktu = ''; String rentangWaktu = '';
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) { if (sewaAset['waktu_mulai'] != null &&
sewaAset['waktu_selesai'] != null) {
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']); waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']); waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
// Format for display // Format for display
final formatTanggal = DateFormat('dd-MM-yyyy'); final formatTanggal = DateFormat('dd-MM-yyyy');
final formatWaktu = DateFormat('HH:mm'); final formatWaktu = DateFormat('HH:mm');
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID'); final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
tanggalSewa = formatTanggalLengkap.format(waktuMulai); tanggalSewa = formatTanggalLengkap.format(waktuMulai);
jamMulai = formatWaktu.format(waktuMulai); jamMulai = formatWaktu.format(waktuMulai);
jamSelesai = formatWaktu.format(waktuSelesai); jamSelesai = formatWaktu.format(waktuSelesai);
// Format based on satuan waktu // Format based on satuan waktu
if (namaSatuanWaktu.toLowerCase() == 'jam') { if (namaSatuanWaktu.toLowerCase() == 'jam') {
// For hours, show time range on same day // For hours, show time range on same day
@ -338,12 +407,13 @@ class WargaSewaController extends GetxController
// Default format // Default format
rentangWaktu = '$jamMulai - $jamSelesai'; rentangWaktu = '$jamMulai - $jamSelesai';
} }
// Full time format for waktuSewa // Full time format for waktuSewa
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - ' waktuSewa =
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}'; '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
} }
// Format price // Format price
String totalPrice = 'Rp 0'; String totalPrice = 'Rp 0';
if (sewaAset['total'] != null) { if (sewaAset['total'] != null) {
@ -354,7 +424,7 @@ class WargaSewaController extends GetxController
); );
totalPrice = formatter.format(sewaAset['total']); totalPrice = formatter.format(sewaAset['total']);
} }
// Add to completed rentals list // Add to completed rentals list
completedRentals.add({ completedRentals.add({
'id': sewaAset['id'] ?? '', 'id': sewaAset['id'] ?? '',
@ -374,35 +444,39 @@ class WargaSewaController extends GetxController
'waktuSelesai': sewaAset['waktu_selesai'], 'waktuSelesai': sewaAset['waktu_selesai'],
}); });
} }
debugPrint('Processed ${completedRentals.length} completed rental records'); 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 {
isLoadingCompleted.value = false; isLoadingCompleted.value = false;
} }
} }
// Load data for the Dibatalkan tab (status: DIBATALKAN) // Load data for the Dibatalkan tab (status: DIBATALKAN)
Future<void> loadCancelledRentals() async { Future<void> loadCancelledRentals() async {
try { try {
isLoadingCancelled.value = true; isLoadingCancelled.value = true;
// Clear existing data // Clear existing data
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 // 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) { if (sewaAset['aset_id'] != null) {
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']); final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
if (asetData != null) { if (asetData != null) {
@ -410,7 +484,7 @@ class WargaSewaController extends GetxController
imageUrl = asetData.imageUrl; imageUrl = asetData.imageUrl;
} }
} }
// Parse waktu mulai and waktu selesai // Parse waktu mulai and waktu selesai
DateTime? waktuMulai; DateTime? waktuMulai;
DateTime? waktuSelesai; DateTime? waktuSelesai;
@ -419,20 +493,21 @@ class WargaSewaController extends GetxController
String jamMulai = ''; String jamMulai = '';
String jamSelesai = ''; String jamSelesai = '';
String rentangWaktu = ''; String rentangWaktu = '';
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) { if (sewaAset['waktu_mulai'] != null &&
sewaAset['waktu_selesai'] != null) {
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']); waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']); waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
// Format for display // Format for display
final formatTanggal = DateFormat('dd-MM-yyyy'); final formatTanggal = DateFormat('dd-MM-yyyy');
final formatWaktu = DateFormat('HH:mm'); final formatWaktu = DateFormat('HH:mm');
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID'); final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
tanggalSewa = formatTanggalLengkap.format(waktuMulai); tanggalSewa = formatTanggalLengkap.format(waktuMulai);
jamMulai = formatWaktu.format(waktuMulai); jamMulai = formatWaktu.format(waktuMulai);
jamSelesai = formatWaktu.format(waktuSelesai); jamSelesai = formatWaktu.format(waktuSelesai);
// Format based on satuan waktu // Format based on satuan waktu
if (namaSatuanWaktu.toLowerCase() == 'jam') { if (namaSatuanWaktu.toLowerCase() == 'jam') {
// For hours, show time range on same day // For hours, show time range on same day
@ -446,12 +521,13 @@ class WargaSewaController extends GetxController
// Default format // Default format
rentangWaktu = '$jamMulai - $jamSelesai'; rentangWaktu = '$jamMulai - $jamSelesai';
} }
// Full time format for waktuSewa // Full time format for waktuSewa
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - ' waktuSewa =
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}'; '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
} }
// Format price // Format price
String totalPrice = 'Rp 0'; String totalPrice = 'Rp 0';
if (sewaAset['total'] != null) { if (sewaAset['total'] != null) {
@ -462,7 +538,7 @@ class WargaSewaController extends GetxController
); );
totalPrice = formatter.format(sewaAset['total']); totalPrice = formatter.format(sewaAset['total']);
} }
// Add to cancelled rentals list // Add to cancelled rentals list
cancelledRentals.add({ cancelledRentals.add({
'id': sewaAset['id'] ?? '', 'id': sewaAset['id'] ?? '',
@ -483,35 +559,40 @@ class WargaSewaController extends GetxController
'alasanPembatalan': sewaAset['alasan_pembatalan'] ?? '-', 'alasanPembatalan': sewaAset['alasan_pembatalan'] ?? '-',
}); });
} }
debugPrint('Processed ${cancelledRentals.length} cancelled rental records'); 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 {
isLoadingCancelled.value = false; isLoadingCancelled.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 {
isLoadingPending.value = true; isLoadingPending.value = true;
// 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 // 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) { if (sewaAset['aset_id'] != null) {
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']); final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
if (asetData != null) { if (asetData != null) {
@ -519,7 +600,7 @@ class WargaSewaController extends GetxController
imageUrl = asetData.imageUrl; imageUrl = asetData.imageUrl;
} }
} }
// Parse waktu mulai and waktu selesai // Parse waktu mulai and waktu selesai
DateTime? waktuMulai; DateTime? waktuMulai;
DateTime? waktuSelesai; DateTime? waktuSelesai;
@ -528,20 +609,21 @@ class WargaSewaController extends GetxController
String jamMulai = ''; String jamMulai = '';
String jamSelesai = ''; String jamSelesai = '';
String rentangWaktu = ''; String rentangWaktu = '';
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) { if (sewaAset['waktu_mulai'] != null &&
sewaAset['waktu_selesai'] != null) {
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']); waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']); waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
// Format for display // Format for display
final formatTanggal = DateFormat('dd-MM-yyyy'); final formatTanggal = DateFormat('dd-MM-yyyy');
final formatWaktu = DateFormat('HH:mm'); final formatWaktu = DateFormat('HH:mm');
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID'); final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
tanggalSewa = formatTanggalLengkap.format(waktuMulai); tanggalSewa = formatTanggalLengkap.format(waktuMulai);
jamMulai = formatWaktu.format(waktuMulai); jamMulai = formatWaktu.format(waktuMulai);
jamSelesai = formatWaktu.format(waktuSelesai); jamSelesai = formatWaktu.format(waktuSelesai);
// Format based on satuan waktu // Format based on satuan waktu
if (namaSatuanWaktu.toLowerCase() == 'jam') { if (namaSatuanWaktu.toLowerCase() == 'jam') {
// For hours, show time range on same day // For hours, show time range on same day
@ -555,12 +637,13 @@ class WargaSewaController extends GetxController
// Default format // Default format
rentangWaktu = '$jamMulai - $jamSelesai'; rentangWaktu = '$jamMulai - $jamSelesai';
} }
// Full time format for waktuSewa // Full time format for waktuSewa
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - ' waktuSewa =
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}'; '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
} }
// Format price // Format price
String totalPrice = 'Rp 0'; String totalPrice = 'Rp 0';
if (sewaAset['total'] != null) { if (sewaAset['total'] != null) {
@ -571,7 +654,7 @@ class WargaSewaController extends GetxController
); );
totalPrice = formatter.format(sewaAset['total']); totalPrice = formatter.format(sewaAset['total']);
} }
// Add to pending rentals list // Add to pending rentals list
pendingRentals.add({ pendingRentals.add({
'id': sewaAset['id'] ?? '', 'id': sewaAset['id'] ?? '',
@ -591,7 +674,7 @@ class WargaSewaController extends GetxController
'waktuSelesai': sewaAset['waktu_selesai'], 'waktuSelesai': sewaAset['waktu_selesai'],
}); });
} }
debugPrint('Processed ${pendingRentals.length} pending rental records'); debugPrint('Processed ${pendingRentals.length} pending rental records');
} catch (e) { } catch (e) {
debugPrint('Error loading pending rentals data: $e'); debugPrint('Error loading pending rentals data: $e');
@ -599,27 +682,27 @@ class WargaSewaController extends GetxController
isLoadingPending.value = false; isLoadingPending.value = false;
} }
} }
// Load data for the Diterima tab (status: DITERIMA) // Load data for the Diterima tab (status: DITERIMA)
Future<void> loadAcceptedRentals() async { Future<void> loadAcceptedRentals() async {
try { try {
isLoadingAccepted.value = true; isLoadingAccepted.value = true;
// Clear existing data // Clear existing data
acceptedRentals.clear(); acceptedRentals.clear();
// Get sewa_aset data with status "DITERIMA" // Get sewa_aset data with status "DITERIMA"
final sewaAsetList = await authProvider.getSewaAsetByStatus(['DITERIMA']); final sewaAsetList = await authProvider.getSewaAsetByStatus(['DITERIMA']);
debugPrint('Fetched ${sewaAsetList.length} accepted sewa_aset records'); debugPrint('Fetched ${sewaAsetList.length} accepted 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 // 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) { if (sewaAset['aset_id'] != null) {
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']); final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
if (asetData != null) { if (asetData != null) {
@ -627,7 +710,7 @@ class WargaSewaController extends GetxController
imageUrl = asetData.imageUrl; imageUrl = asetData.imageUrl;
} }
} }
// Parse waktu mulai and waktu selesai // Parse waktu mulai and waktu selesai
DateTime? waktuMulai; DateTime? waktuMulai;
DateTime? waktuSelesai; DateTime? waktuSelesai;
@ -636,20 +719,21 @@ class WargaSewaController extends GetxController
String jamMulai = ''; String jamMulai = '';
String jamSelesai = ''; String jamSelesai = '';
String rentangWaktu = ''; String rentangWaktu = '';
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) { if (sewaAset['waktu_mulai'] != null &&
sewaAset['waktu_selesai'] != null) {
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']); waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']); waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
// Format for display // Format for display
final formatTanggal = DateFormat('dd-MM-yyyy'); final formatTanggal = DateFormat('dd-MM-yyyy');
final formatWaktu = DateFormat('HH:mm'); final formatWaktu = DateFormat('HH:mm');
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID'); final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
tanggalSewa = formatTanggalLengkap.format(waktuMulai); tanggalSewa = formatTanggalLengkap.format(waktuMulai);
jamMulai = formatWaktu.format(waktuMulai); jamMulai = formatWaktu.format(waktuMulai);
jamSelesai = formatWaktu.format(waktuSelesai); jamSelesai = formatWaktu.format(waktuSelesai);
// Format based on satuan waktu // Format based on satuan waktu
if (namaSatuanWaktu.toLowerCase() == 'jam') { if (namaSatuanWaktu.toLowerCase() == 'jam') {
// For hours, show time range on same day // For hours, show time range on same day
@ -663,12 +747,13 @@ class WargaSewaController extends GetxController
// Default format // Default format
rentangWaktu = '$jamMulai - $jamSelesai'; rentangWaktu = '$jamMulai - $jamSelesai';
} }
// Full time format for waktuSewa // Full time format for waktuSewa
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - ' waktuSewa =
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}'; '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
} }
// Format price // Format price
String totalPrice = 'Rp 0'; String totalPrice = 'Rp 0';
if (sewaAset['total'] != null) { if (sewaAset['total'] != null) {
@ -679,7 +764,7 @@ class WargaSewaController extends GetxController
); );
totalPrice = formatter.format(sewaAset['total']); totalPrice = formatter.format(sewaAset['total']);
} }
// Add to accepted rentals list // Add to accepted rentals list
acceptedRentals.add({ acceptedRentals.add({
'id': sewaAset['id'] ?? '', 'id': sewaAset['id'] ?? '',
@ -699,7 +784,7 @@ class WargaSewaController extends GetxController
'waktuSelesai': sewaAset['waktu_selesai'], 'waktuSelesai': sewaAset['waktu_selesai'],
}); });
} }
debugPrint('Processed ${acceptedRentals.length} accepted rental records'); debugPrint('Processed ${acceptedRentals.length} accepted rental records');
} catch (e) { } catch (e) {
debugPrint('Error loading accepted rentals data: $e'); debugPrint('Error loading accepted rentals data: $e');
@ -707,4 +792,166 @@ class WargaSewaController extends GetxController
isLoadingAccepted.value = false; isLoadingAccepted.value = false;
} }
} }
Future<void> loadReturnedRentals() async {
try {
isLoadingReturned.value = true;
returnedRentals.clear();
final sewaAsetList = await authProvider.getSewaAsetByStatus([
'DIKEMBALIKAN',
]);
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']);
}
returnedRentals.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'] ?? 'DIKEMBALIKAN',
'totalPrice': totalPrice,
'tanggalSewa': tanggalSewa,
'jamMulai': jamMulai,
'jamSelesai': jamSelesai,
'rentangWaktu': rentangWaktu,
'namaSatuanWaktu': namaSatuanWaktu,
'waktuMulai': sewaAset['waktu_mulai'],
'waktuSelesai': sewaAset['waktu_selesai'],
});
}
} catch (e) {
debugPrint('Error loading returned rentals data: $e');
} finally {
isLoadingReturned.value = false;
}
}
Future<void> loadActiveRentals() async {
try {
isLoadingActive.value = true;
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']);
}
activeRentals.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'] ?? 'AKTIF',
'totalPrice': totalPrice,
'tanggalSewa': tanggalSewa,
'jamMulai': jamMulai,
'jamSelesai': jamSelesai,
'rentangWaktu': rentangWaktu,
'namaSatuanWaktu': namaSatuanWaktu,
'waktuMulai': sewaAset['waktu_mulai'],
'waktuSelesai': sewaAset['waktu_selesai'],
});
}
} catch (e) {
debugPrint('Error loading active rentals data: $e');
} finally {
isLoadingActive.value = false;
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -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,

View File

@ -154,10 +154,10 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
'route': () => controller.navigateToRentals(), 'route': () => controller.navigateToRentals(),
}, },
{ {
'title': 'Bayar', 'title': 'Paket',
'icon': Icons.payment_outlined, 'icon': Icons.widgets_outlined,
'color': const Color(0xFF2196F3), 'color': const Color(0xFF2196F3),
'route': () => Get.toNamed(Routes.PEMBAYARAN_SEWA), 'route': () => controller.toSewaAsetTabPaket(),
}, },
]; ];
@ -218,32 +218,44 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
child: Column( child: Column(
children: [ children: [
// Sewa Diterima // Sewa Diterima
_buildActivityCard( Obx(
title: 'Sewa Diterima', () => _buildActivityCard(
value: controller.activeRentals.length.toString(), title: 'Sewa Diterima',
icon: Icons.check_circle_outline, value: controller.diterimaCount.value.toString(),
color: AppColors.success, icon: Icons.check_circle_outline,
onTap: () => controller.navigateToRentals(), color: AppColors.success,
onTap:
() =>
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 2}),
),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Tagihan Aktif // Tagihan Aktif
_buildActivityCard( Obx(
title: 'Tagihan Aktif', () => _buildActivityCard(
value: controller.activeBills.length.toString(), title: 'Tagihan Aktif',
icon: Icons.receipt_long_outlined, value: controller.tagihanAktifCount.value.toString(),
color: AppColors.warning, icon: Icons.receipt_long_outlined,
onTap: () => Get.toNamed(Routes.PEMBAYARAN_SEWA), color: AppColors.warning,
onTap:
() =>
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 0}),
),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Denda Aktif // Denda Aktif
_buildActivityCard( Obx(
title: 'Denda Aktif', () => _buildActivityCard(
value: controller.activePenalties.length.toString(), title: 'Denda Aktif',
icon: Icons.warning_amber_outlined, value: controller.dendaAktifCount.value.toString(),
color: AppColors.error, icon: Icons.warning_amber_outlined,
onTap: () => Get.toNamed(Routes.PEMBAYARAN_SEWA), color: AppColors.error,
onTap:
() =>
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 0}),
),
), ),
], ],
), ),
@ -357,7 +369,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'Sewa Diterima', 'Sewa Aktif',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -498,31 +510,34 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
), ),
child: Row( child: Row(
children: [ children: [
// Asset icon // Asset icon/gambar
Container( Container(
padding: const EdgeInsets.all(12), width: 48,
height: 48,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColors.primary.withOpacity(0.7),
AppColors.primary,
],
),
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
boxShadow: [ color: AppColors.primary.withOpacity(0.08),
BoxShadow(
color: AppColors.primary.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
), ),
child: const Icon( child: ClipRRect(
Icons.local_shipping, borderRadius: BorderRadius.circular(14),
color: Colors.white, child:
size: 24, rental['imageUrl'] != null &&
rental['imageUrl'].toString().isNotEmpty
? Image.network(
rental['imageUrl'],
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) => Icon(
Icons.local_shipping,
color: AppColors.primary,
size: 28,
),
)
: Icon(
Icons.local_shipping,
color: AppColors.primary,
size: 28,
),
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
@ -533,7 +548,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
rental['name'], rental['name'] ?? '-',
style: TextStyle( style: TextStyle(
fontSize: 17, fontSize: 17,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -542,7 +557,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
rental['time'], rental['waktuSewa'] ?? '',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: AppColors.textSecondary, color: AppColors.textSecondary,
@ -567,7 +582,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
), ),
), ),
child: Text( child: Text(
rental['price'], rental['totalPrice'] ?? 'Rp 0',
style: TextStyle( style: TextStyle(
color: AppColors.primary, color: AppColors.primary,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -591,14 +606,14 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
child: _buildInfoItem( child: _buildInfoItem(
icon: Icons.timer_outlined, icon: Icons.timer_outlined,
title: 'Durasi', title: 'Durasi',
value: rental['duration'], value: rental['duration'] ?? '-',
), ),
), ),
Expanded( Expanded(
child: _buildInfoItem( child: _buildInfoItem(
icon: Icons.calendar_today_outlined, icon: Icons.calendar_today_outlined,
title: 'Status', title: 'Status',
value: 'Diterima', value: rental['status'] ?? '-',
valueColor: AppColors.success, valueColor: AppColors.success,
), ),
), ),
@ -608,7 +623,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
const SizedBox(height: 16), const SizedBox(height: 16),
// Action buttons // Action buttons
if (rental['can_extend']) if ((rental['can_extend'] ?? false) == true)
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () => controller.extendRental(rental['id']), onPressed: () => controller.extendRental(rental['id']),
icon: const Icon(Icons.update, size: 18), icon: const Icon(Icons.update, size: 18),

View File

@ -3,12 +3,16 @@ import 'package:get/get.dart';
import '../controllers/warga_dashboard_controller.dart'; import '../controllers/warga_dashboard_controller.dart';
import '../views/warga_layout.dart'; import '../views/warga_layout.dart';
import '../../../theme/app_colors.dart'; import '../../../theme/app_colors.dart';
import '../../../widgets/app_drawer.dart';
import '../../../services/navigation_service.dart';
class WargaProfileView extends GetView<WargaDashboardController> { class WargaProfileView extends GetView<WargaDashboardController> {
const WargaProfileView({super.key}); const WargaProfileView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final navigationService = Get.find<NavigationService>();
navigationService.setNavIndex(2);
return WargaLayout( return WargaLayout(
appBar: AppBar( appBar: AppBar(
title: const Text('Profil Saya'), title: const Text('Profil Saya'),
@ -29,6 +33,14 @@ class WargaProfileView extends GetView<WargaDashboardController> {
), ),
], ],
), ),
drawer: AppDrawer(
onNavItemTapped: (index) {
// Handle navigation if needed
},
onLogout: () {
controller.logout();
},
),
backgroundColor: Colors.grey.shade100, backgroundColor: Colors.grey.shade100,
body: RefreshIndicator( body: RefreshIndicator(
color: AppColors.primary, color: AppColors.primary,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,88 @@
import 'package:supabase_flutter/supabase_flutter.dart';
import '../data/models/pembayaran_model.dart';
class PembayaranService {
final SupabaseClient _supabase = Supabase.instance.client;
/// Ambil data pembayaran antara [start] (inklusif) dan [end] (eksklusif).
Future<List<PembayaranModel>> _fetchBetween(
DateTime start,
DateTime end,
) async {
final data = await _supabase
.from('pembayaran')
.select('id, metode_pembayaran, total_pembayaran, waktu_pembayaran')
.gte('waktu_pembayaran', start.toIso8601String())
.lt('waktu_pembayaran', end.toIso8601String())
.order('waktu_pembayaran', ascending: true);
return (data as List<dynamic>)
.map((e) => PembayaranModel.fromJson(e as Map<String, dynamic>))
.toList();
}
/// Hitung statistik yang diminta.
Future<Map<String, dynamic>> fetchStats() async {
final now = DateTime.now();
// Rentang bulan ini: [awal bulan ini, awal bulan depan)
final thisMonthStart = DateTime(now.year, now.month, 1);
final nextMonthStart = DateTime(now.year, now.month + 1, 1);
// Bulan lalu: [awal bulan lalu, awal bulan ini)
final lastMonthStart = DateTime(now.year, now.month - 1, 1);
final thisMonthEnd = thisMonthStart;
// 6 bulan terakhir: [6 bulan lalu, sekarang]
final sixMonthsAgo = DateTime(now.year, now.month - 6, 1);
// 1) Data bulan ini & bulan lalu
final thisMonthData = await _fetchBetween(thisMonthStart, nextMonthStart);
final lastMonthData = await _fetchBetween(lastMonthStart, thisMonthEnd);
// 2) Data 6 bulan terakhir
final sixMonthsData = await _fetchBetween(sixMonthsAgo, nextMonthStart);
// 3) Hitung total pendapatan
double sum(List<PembayaranModel> list) =>
list.fold(0.0, (acc, e) => acc + e.totalPembayaran);
final totalThis = sum(thisMonthData);
final totalLast = sum(lastMonthData);
final totalSix = sum(sixMonthsData);
// 4) Persentase selisih (bulanan)
double percentDiff = 0.0;
if (totalLast != 0) {
percentDiff = ((totalThis - totalLast) / totalLast) * 100;
}
// 5) Total per metode (hanya dari bulan ini, misalnya)
double totTunai = 0.0, totTransfer = 0.0;
for (var p in thisMonthData) {
if (p.metodePembayaran.toLowerCase() == 'tunai') {
totTunai += p.totalPembayaran;
} else if (p.metodePembayaran.toLowerCase() == 'transfer') {
totTransfer += p.totalPembayaran;
}
}
// 6) Trend per month (6 months, oldest to newest)
List<double> trendPerMonth = [];
for (int i = 5; i >= 0; i--) {
final dt = DateTime(now.year, now.month - i, 1);
final dtNext = DateTime(now.year, now.month - i + 1, 1);
final monthData = await _fetchBetween(dt, dtNext);
trendPerMonth.add(sum(monthData));
}
return {
'totalThisMonth': totalThis,
'percentComparedLast': percentDiff,
'totalTunai': totTunai,
'totalTransfer': totTransfer,
'totalLastSixMonths': totalSix,
'trendPerMonth': trendPerMonth,
};
}
}

View File

@ -2,6 +2,8 @@ import 'package:get/get.dart';
import 'navigation_service.dart'; import 'navigation_service.dart';
import '../data/providers/auth_provider.dart'; import '../data/providers/auth_provider.dart';
import '../modules/warga/controllers/warga_dashboard_controller.dart'; import '../modules/warga/controllers/warga_dashboard_controller.dart';
import '../data/providers/aset_provider.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
/// Abstract class untuk mengelola lifecycle service dan dependency /// Abstract class untuk mengelola lifecycle service dan dependency
abstract class ServiceManager { abstract class ServiceManager {
@ -26,6 +28,11 @@ abstract class ServiceManager {
Get.put(AuthProvider(), permanent: true); Get.put(AuthProvider(), permanent: true);
} }
// Register AsetProvider if not already registered
if (!Get.isRegistered<AsetProvider>()) {
Get.put(AsetProvider(), permanent: true);
}
// Register WargaDashboardController as a permanent controller // Register WargaDashboardController as a permanent controller
// This ensures it's always available for the drawer // This ensures it's always available for the drawer
registerWargaDashboardController(); registerWargaDashboardController();

View File

@ -0,0 +1,222 @@
import 'package:supabase_flutter/supabase_flutter.dart';
import '../data/models/rental_booking_model.dart';
import 'package:flutter/foundation.dart';
class SewaService {
final SupabaseClient _supabase = Supabase.instance.client;
Future<List<SewaModel>> fetchAllSewa() async {
// 1) Ambil semua sewa_aset
final sewaData =
await _supabase.from('sewa_aset').select('''
id,
user_id,
status,
waktu_mulai,
waktu_selesai,
tanggal_pemesanan,
tipe_pesanan,
kuantitas,
aset_id,
paket_id
''')
as List<dynamic>;
if (sewaData.isEmpty) return [];
// Konversi dasar ke map
final List<Map<String, dynamic>> rawSewa =
sewaData.map((e) => e as Map<String, dynamic>).toList();
// Kumpulkan semua ID
final sewaIds = rawSewa.map((e) => e['id'] as String).toList();
final userIds = rawSewa.map((e) => e['user_id'] as String).toSet().toList();
// Pisahkan aset dan paket IDs
final asetIds =
rawSewa
.where((e) => e['tipe_pesanan'] == 'tunggal')
.map((e) => e['aset_id'] as String?)
.whereType<String>()
.toSet()
.toList();
final paketIds =
rawSewa
.where((e) => e['tipe_pesanan'] == 'paket')
.map((e) => e['paket_id'] as String?)
.whereType<String>()
.toSet()
.toList();
// 2) Ambil tagihan_sewa
final tagihanData =
await _supabase
.from('tagihan_sewa')
.select('sewa_aset_id, total_tagihan, denda, tagihan_dibayar')
.filter('sewa_aset_id', 'in', '(${sewaIds.join(",")})')
as List<dynamic>;
final Map<String, Map<String, dynamic>> mapTagihan = {
for (var t in tagihanData)
t['sewa_aset_id'] as String: t as Map<String, dynamic>,
};
// 3) Ambil data warga_desa
final wargaData =
await _supabase
.from('warga_desa')
.select('user_id, nama_lengkap, no_hp, avatar')
.filter('user_id', 'in', '(${userIds.join(",")})')
as List<dynamic>;
debugPrint('DEBUG wargaData (raw): ' + wargaData.toString());
final Map<String, Map<String, String>> mapWarga = {
for (var w in wargaData)
(w['user_id'] as String): {
'nama': w['nama_lengkap'] as String? ?? '-',
'noHp': w['no_hp'] as String? ?? '-',
'avatar': w['avatar'] as String? ?? '-',
},
};
debugPrint('DEBUG mapWarga (mapped): ' + mapWarga.toString());
// 4) Ambil data aset (untuk tunggal)
Map<String, Map<String, String>> mapAset = {};
if (asetIds.isNotEmpty) {
final asetData =
await _supabase
.from('aset')
.select('id, nama')
.filter('id', 'in', '(${asetIds.join(",")})')
as List<dynamic>;
final fotoData =
await _supabase
.from('foto_aset')
.select('id_aset, foto_aset')
.filter('id_aset', 'in', '(${asetIds.join(",")})')
as List<dynamic>;
// Map aset id → nama
mapAset = {
for (var a in asetData)
(a['id'] as String): {'nama': a['nama'] as String},
};
// Ambil foto pertama per aset
for (var f in fotoData) {
final aid = f['id_aset'] as String;
if (mapAset.containsKey(aid) && mapAset[aid]!['foto'] == null) {
mapAset[aid]!['foto'] = f['foto_aset'] as String;
}
}
}
// 5) Ambil data paket (untuk paket)
Map<String, Map<String, String>> mapPaket = {};
if (paketIds.isNotEmpty) {
final paketData =
await _supabase
.from('paket')
.select('id, nama')
.filter('id', 'in', '(${paketIds.join(",")})')
as List<dynamic>;
final fotoData =
await _supabase
.from('foto_aset')
.select('id_paket, foto_aset')
.filter('id_paket', 'in', '(${paketIds.join(",")})')
as List<dynamic>;
mapPaket = {
for (var p in paketData)
(p['id'] as String): {'nama': p['nama'] as String},
};
for (var f in fotoData) {
final pid = f['id_paket'] as String;
if (mapPaket.containsKey(pid) && mapPaket[pid]!['foto'] == null) {
mapPaket[pid]!['foto'] = f['foto_aset'] as String;
}
}
}
// Debug print hasil query utama
debugPrint('sewaData: ' + sewaData.toString());
debugPrint('sewaData count: ' + sewaData.length.toString());
debugPrint('tagihanData: ' + tagihanData.toString());
debugPrint('wargaData: ' + wargaData.toString());
debugPrint('mapAset: ' + mapAset.toString());
debugPrint('mapPaket: ' + mapPaket.toString());
// 6) Gabungkan dan bangun SewaModel
final List<SewaModel> result = [];
for (var row in rawSewa) {
final id = row['id'] as String;
final tipe = row['tipe_pesanan'] as String;
final warga = mapWarga[row['user_id']];
final tagihan = mapTagihan[id];
if (warga == null || tagihan == null) {
// Skip data jika relasi tidak ditemukan
continue;
}
result.add(
SewaModel(
id: id,
userId: row['user_id'],
status: row['status'] as String,
waktuMulai: DateTime.parse(row['waktu_mulai'] as String),
waktuSelesai: DateTime.parse(row['waktu_selesai'] as String),
tanggalPemesanan: DateTime.parse(row['tanggal_pemesanan'] as String),
tipePesanan: tipe,
kuantitas: row['kuantitas'] as int,
asetId: tipe == 'tunggal' ? row['aset_id'] as String : null,
asetNama:
(row['aset_id'] != null &&
tipe == 'tunggal' &&
mapAset[row['aset_id']] != null)
? mapAset[row['aset_id']]!['nama']
: null,
asetFoto:
(row['aset_id'] != null &&
tipe == 'tunggal' &&
mapAset[row['aset_id']] != null)
? mapAset[row['aset_id']]!['foto']
: null,
paketId: tipe == 'paket' ? row['paket_id'] as String : null,
paketNama:
(row['paket_id'] != null &&
tipe == 'paket' &&
mapPaket[row['paket_id']] != null)
? mapPaket[row['paket_id']]!['nama']
: null,
paketFoto:
(row['paket_id'] != null &&
tipe == 'paket' &&
mapPaket[row['paket_id']] != null)
? mapPaket[row['paket_id']]!['foto']
: null,
totalTagihan:
tagihan['total_tagihan'] != null
? (tagihan['total_tagihan'] as num?)?.toDouble() ?? 0.0
: 0.0,
denda:
tagihan['denda'] != null
? (tagihan['denda'] as num?)?.toDouble()
: null,
dibayar:
tagihan['tagihan_dibayar'] != null
? (tagihan['tagihan_dibayar'] as num?)?.toDouble()
: null,
paidAmount:
tagihan['total_tagihan'] != null
? (tagihan['total_tagihan'] as num?)?.toDouble()
: null,
wargaNama: warga['nama'] ?? '-',
wargaNoHp: warga['noHp'] ?? '-',
wargaAvatar: warga['avatar'] ?? '-',
),
);
}
// Debug print hasil query result
debugPrint('SewaModel result: ' + result.toString());
debugPrint('SewaModel result count: ' + result.length.toString());
return result;
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'app_colors.dart'; import 'app_colors.dart';
/// App theme configuration /// App theme configuration
@ -19,13 +20,16 @@ class AppTheme {
), ),
scaffoldBackgroundColor: AppColors.background, scaffoldBackgroundColor: AppColors.background,
// Set Lato as the default font for the entire app
fontFamily: GoogleFonts.lato().fontFamily,
// App bar theme // App bar theme
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
backgroundColor: AppColors.primary, backgroundColor: AppColors.primary,
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 0, elevation: 0,
iconTheme: const IconThemeData(color: Colors.white), iconTheme: const IconThemeData(color: Colors.white),
titleTextStyle: const TextStyle( titleTextStyle: GoogleFonts.lato(
color: Colors.white, color: Colors.white,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -50,7 +54,10 @@ class AppTheme {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), textStyle: GoogleFonts.lato(
fontSize: 16,
fontWeight: FontWeight.w600,
),
), ),
), ),
@ -62,7 +69,10 @@ class AppTheme {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), textStyle: GoogleFonts.lato(
fontSize: 16,
fontWeight: FontWeight.w600,
),
), ),
), ),
@ -70,7 +80,10 @@ class AppTheme {
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: AppColors.primary, foregroundColor: AppColors.primary,
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), textStyle: GoogleFonts.lato(
fontSize: 14,
fontWeight: FontWeight.w600,
),
), ),
), ),
@ -98,8 +111,8 @@ class AppTheme {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: AppColors.error, width: 1.5), borderSide: BorderSide(color: AppColors.error, width: 1.5),
), ),
hintStyle: TextStyle(color: AppColors.textLight), hintStyle: GoogleFonts.lato(color: AppColors.textLight),
labelStyle: TextStyle(color: AppColors.textSecondary), labelStyle: GoogleFonts.lato(color: AppColors.textSecondary),
), ),
// Checkbox theme // Checkbox theme
@ -115,21 +128,21 @@ class AppTheme {
// Text themes // Text themes
textTheme: TextTheme( textTheme: TextTheme(
displayLarge: TextStyle(color: AppColors.textPrimary), displayLarge: GoogleFonts.lato(color: AppColors.textPrimary),
displayMedium: TextStyle(color: AppColors.textPrimary), displayMedium: GoogleFonts.lato(color: AppColors.textPrimary),
displaySmall: TextStyle(color: AppColors.textPrimary), displaySmall: GoogleFonts.lato(color: AppColors.textPrimary),
headlineLarge: TextStyle(color: AppColors.textPrimary), headlineLarge: GoogleFonts.lato(color: AppColors.textPrimary),
headlineMedium: TextStyle(color: AppColors.textPrimary), headlineMedium: GoogleFonts.lato(color: AppColors.textPrimary),
headlineSmall: TextStyle(color: AppColors.textPrimary), headlineSmall: GoogleFonts.lato(color: AppColors.textPrimary),
titleLarge: TextStyle(color: AppColors.textPrimary), titleLarge: GoogleFonts.lato(color: AppColors.textPrimary),
titleMedium: TextStyle(color: AppColors.textPrimary), titleMedium: GoogleFonts.lato(color: AppColors.textPrimary),
titleSmall: TextStyle(color: AppColors.textPrimary), titleSmall: GoogleFonts.lato(color: AppColors.textPrimary),
bodyLarge: TextStyle(color: AppColors.textPrimary), bodyLarge: GoogleFonts.lato(color: AppColors.textPrimary),
bodyMedium: TextStyle(color: AppColors.textPrimary), bodyMedium: GoogleFonts.lato(color: AppColors.textPrimary),
bodySmall: TextStyle(color: AppColors.textSecondary), bodySmall: GoogleFonts.lato(color: AppColors.textSecondary),
labelLarge: TextStyle(color: AppColors.textPrimary), labelLarge: GoogleFonts.lato(color: AppColors.textPrimary),
labelMedium: TextStyle(color: AppColors.textSecondary), labelMedium: GoogleFonts.lato(color: AppColors.textSecondary),
labelSmall: TextStyle(color: AppColors.textLight), labelSmall: GoogleFonts.lato(color: AppColors.textLight),
), ),
// Divider theme // Divider theme

View File

@ -0,0 +1,10 @@
import 'package:intl/intl.dart';
String formatRupiah(double value) {
final formatter = NumberFormat.currency(
locale: 'id_ID',
symbol: 'Rp',
decimalDigits: 0,
);
return formatter.format(value);
}

View File

@ -291,34 +291,6 @@ class AppDrawer extends StatelessWidget {
), ),
// Settings Items // Settings Items
_buildDrawerItem(
icon: Icons.info_outline_rounded,
title: 'Tentang Aplikasi',
subtitle: 'Informasi dan bantuan',
showTrailing: false,
onTap: () {
Navigator.pop(context);
// Show about dialog
showAboutDialog(
context: context,
applicationName: 'BumRent App',
applicationVersion: '1.0.0',
applicationIcon: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
'assets/images/logo.png',
width: 40,
height: 40,
),
),
children: [
const Text(
'Aplikasi penyewaan dan berlangganan aset milik BUMDes untuk warga desa.',
),
],
);
},
),
_buildDrawerItem( _buildDrawerItem(
icon: Icons.logout_rounded, icon: Icons.logout_rounded,
title: 'Keluar', title: 'Keluar',

View File

@ -525,6 +525,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.1" version: "5.1.1"
logger:
dependency: "direct main"
description:
name: logger
sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1
url: "https://pub.dev"
source: hosted
version: "2.5.0"
logging: logging:
dependency: transitive dependency: transitive
description: description:

View File

@ -48,6 +48,7 @@ dependencies:
flutter_dotenv: ^5.1.0 flutter_dotenv: ^5.1.0
image_picker: ^1.0.7 image_picker: ^1.0.7
intl: 0.19.0 intl: 0.19.0
logger: ^2.1.0
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
get_storage: ^2.1.1 get_storage: ^2.1.1

30
widget_test.dart Normal file
View File

@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:bumrent_app/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}