fitur petugas
This commit is contained in:
@ -5,6 +5,7 @@ class AsetModel {
|
||||
final String nama;
|
||||
final String deskripsi;
|
||||
final String kategori;
|
||||
final String jenis; // Add this line
|
||||
final int harga;
|
||||
final int? denda;
|
||||
final String status;
|
||||
@ -14,17 +15,21 @@ class AsetModel {
|
||||
final int? kuantitasTerpakai;
|
||||
final String? satuanUkur;
|
||||
|
||||
// Untuk menampung URL gambar pertama dari tabel foto_aset
|
||||
// URL gambar utama (untuk backward compatibility)
|
||||
String? imageUrl;
|
||||
|
||||
// List untuk menyimpan semua URL gambar aset
|
||||
final RxList<String> imageUrls = <String>[].obs;
|
||||
|
||||
// Menggunakan RxList untuk membuatnya mutable dan reaktif
|
||||
RxList<Map<String, dynamic>> satuanWaktuSewa = <Map<String, dynamic>>[].obs;
|
||||
final RxList<Map<String, dynamic>> satuanWaktuSewa = <Map<String, dynamic>>[].obs;
|
||||
|
||||
AsetModel({
|
||||
required this.id,
|
||||
required this.nama,
|
||||
required this.deskripsi,
|
||||
required this.kategori,
|
||||
this.jenis = 'Sewa', // Add this line with default value
|
||||
required this.harga,
|
||||
this.denda,
|
||||
required this.status,
|
||||
@ -42,31 +47,69 @@ class AsetModel {
|
||||
}
|
||||
}
|
||||
|
||||
// Menambahkan URL gambar dari JSON
|
||||
void addImageUrl(String? url) {
|
||||
if (url != null && url.isNotEmpty && !imageUrls.contains(url)) {
|
||||
imageUrls.add(url);
|
||||
// Update imageUrl untuk backward compatibility
|
||||
if (imageUrl == null) {
|
||||
imageUrl = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Menghapus URL gambar
|
||||
bool removeImageUrl(String url) {
|
||||
final removed = imageUrls.remove(url);
|
||||
if (removed && imageUrl == url) {
|
||||
imageUrl = imageUrls.isNotEmpty ? imageUrls.first : null;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
factory AsetModel.fromJson(Map<String, dynamic> json) {
|
||||
return AsetModel(
|
||||
final model = AsetModel(
|
||||
id: json['id'] ?? '',
|
||||
nama: json['nama'] ?? '',
|
||||
deskripsi: json['deskripsi'] ?? '',
|
||||
kategori: json['kategori'] ?? '',
|
||||
jenis: json['jenis'] ?? 'Sewa',
|
||||
harga: json['harga'] ?? 0,
|
||||
denda: json['denda'],
|
||||
status: json['status'] ?? '',
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
kuantitas: json['kuantitas'],
|
||||
kuantitasTerpakai: json['kuantitas_terpakai'],
|
||||
satuanUkur: json['satuan_ukur'],
|
||||
imageUrl: json['foto_aset'],
|
||||
initialSatuanWaktuSewa: json['satuan_waktu_sewa'] != null
|
||||
? List<Map<String, dynamic>>.from(json['satuan_waktu_sewa'])
|
||||
: null,
|
||||
);
|
||||
|
||||
// Add the main image URL to the list if it exists
|
||||
if (json['foto_aset'] != null) {
|
||||
model.addImageUrl(json['foto_aset']);
|
||||
}
|
||||
|
||||
// Add any additional image URLs if they exist in the JSON
|
||||
if (json['foto_aset_tambahan'] != null) {
|
||||
final additionalImages = List<String>.from(json['foto_aset_tambahan']);
|
||||
for (final url in additionalImages) {
|
||||
model.addImageUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
final data = <String, dynamic>{
|
||||
'id': id,
|
||||
'nama': nama,
|
||||
'deskripsi': deskripsi,
|
||||
@ -80,5 +123,23 @@ class AsetModel {
|
||||
'kuantitas_terpakai': kuantitasTerpakai,
|
||||
'satuan_ukur': satuanUkur,
|
||||
};
|
||||
|
||||
// Add image URLs if they exist
|
||||
if (imageUrls.isNotEmpty) {
|
||||
data['foto_aset'] = imageUrl;
|
||||
|
||||
// Add additional images (excluding the main image)
|
||||
final additionalImages = imageUrls.where((url) => url != imageUrl).toList();
|
||||
if (additionalImages.isNotEmpty) {
|
||||
data['foto_aset_tambahan'] = additionalImages;
|
||||
}
|
||||
}
|
||||
|
||||
// Add rental time units if they exist
|
||||
if (satuanWaktuSewa.isNotEmpty) {
|
||||
data['satuan_waktu_sewa'] = satuanWaktuSewa.toList();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
class PaketModel {
|
||||
final String id;
|
||||
@ -6,12 +7,13 @@ class PaketModel {
|
||||
final String deskripsi;
|
||||
final double harga;
|
||||
final int kuantitas;
|
||||
final List<String> foto;
|
||||
final List<Map<String, dynamic>> satuanWaktuSewa;
|
||||
final String status;
|
||||
List<String> foto;
|
||||
List<Map<String, dynamic>> satuanWaktuSewa;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String? foto_paket; // Main photo URL
|
||||
final List<String>? images; // List of photo URLs
|
||||
String? foto_paket; // Main photo URL
|
||||
List<String>? images; // List of photo URLs
|
||||
|
||||
PaketModel({
|
||||
required this.id,
|
||||
@ -19,13 +21,47 @@ class PaketModel {
|
||||
required this.deskripsi,
|
||||
required this.harga,
|
||||
required this.kuantitas,
|
||||
required this.foto,
|
||||
required this.satuanWaktuSewa,
|
||||
this.status = 'aktif',
|
||||
required List<String> foto,
|
||||
required List<Map<String, dynamic>> satuanWaktuSewa,
|
||||
this.foto_paket,
|
||||
this.images,
|
||||
List<String>? images,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
}) : foto = List.from(foto),
|
||||
satuanWaktuSewa = List.from(satuanWaktuSewa),
|
||||
images = images != null ? List.from(images) : [];
|
||||
|
||||
// Add copyWith method for immutability patterns
|
||||
PaketModel copyWith({
|
||||
String? id,
|
||||
String? nama,
|
||||
String? deskripsi,
|
||||
double? harga,
|
||||
int? kuantitas,
|
||||
String? status,
|
||||
List<String>? foto,
|
||||
List<Map<String, dynamic>>? satuanWaktuSewa,
|
||||
String? foto_paket,
|
||||
List<String>? images,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return PaketModel(
|
||||
id: id ?? this.id,
|
||||
nama: nama ?? this.nama,
|
||||
deskripsi: deskripsi ?? this.deskripsi,
|
||||
harga: harga ?? this.harga,
|
||||
kuantitas: kuantitas ?? this.kuantitas,
|
||||
status: status ?? this.status,
|
||||
foto: foto ?? List.from(this.foto),
|
||||
satuanWaktuSewa: satuanWaktuSewa ?? List.from(this.satuanWaktuSewa),
|
||||
foto_paket: foto_paket ?? this.foto_paket,
|
||||
images: images ?? (this.images != null ? List.from(this.images!) : null),
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
// Alias for fromJson to maintain compatibility
|
||||
factory PaketModel.fromMap(Map<String, dynamic> json) => PaketModel.fromJson(json);
|
||||
@ -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(
|
||||
id: json['id']?.toString() ?? '',
|
||||
nama: json['nama']?.toString() ?? '',
|
||||
deskripsi: json['deskripsi']?.toString() ?? '',
|
||||
status: status,
|
||||
harga: (json['harga'] is num) ? (json['harga'] as num).toDouble() : 0.0,
|
||||
kuantitas: (json['kuantitas'] is num) ? (json['kuantitas'] as num).toInt() : 1,
|
||||
foto: fotoList,
|
||||
@ -97,34 +138,6 @@ class PaketModel {
|
||||
'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
|
||||
String get firstPhotoUrl => foto.isNotEmpty ? foto.first : '';
|
||||
|
22
lib/app/data/models/pembayaran_model.dart
Normal file
22
lib/app/data/models/pembayaran_model.dart
Normal file
@ -0,0 +1,22 @@
|
||||
class PembayaranModel {
|
||||
final String id;
|
||||
final int totalPembayaran;
|
||||
final String metodePembayaran;
|
||||
final DateTime waktuPembayaran;
|
||||
|
||||
PembayaranModel({
|
||||
required this.id,
|
||||
required this.totalPembayaran,
|
||||
required this.metodePembayaran,
|
||||
required this.waktuPembayaran,
|
||||
});
|
||||
|
||||
factory PembayaranModel.fromJson(Map<String, dynamic> json) {
|
||||
return PembayaranModel(
|
||||
id: json['id'] as String,
|
||||
totalPembayaran: json['total_pembayaran'] as int,
|
||||
metodePembayaran: json['metode_pembayaran'] as String,
|
||||
waktuPembayaran: DateTime.parse(json['waktu_pembayaran'] as String),
|
||||
);
|
||||
}
|
||||
}
|
@ -1 +1,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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.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_sewa_model.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../models/paket_model.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
class AsetProvider extends GetxService {
|
||||
late final SupabaseClient client;
|
||||
@ -24,8 +28,16 @@ class AsetProvider extends GetxService {
|
||||
.from('aset')
|
||||
.select('*')
|
||||
.eq('kategori', 'sewa')
|
||||
.eq('status', 'tersedia') // Hanya yang tersedia
|
||||
.order('nama', ascending: true); // Urutan berdasarkan nama
|
||||
.ilike('status', 'tersedia') // Hanya yang tersedia
|
||||
.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');
|
||||
|
||||
@ -56,8 +68,16 @@ class AsetProvider extends GetxService {
|
||||
.from('aset')
|
||||
.select('*')
|
||||
.eq('kategori', 'langganan')
|
||||
.eq('status', 'tersedia') // Hanya yang tersedia
|
||||
.order('nama', ascending: true); // Urutan berdasarkan nama
|
||||
.ilike('status', 'tersedia') // Hanya yang tersedia
|
||||
.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');
|
||||
|
||||
@ -120,9 +140,26 @@ class AsetProvider extends GetxService {
|
||||
Future<void> loadAssetPhotos(AsetModel aset) async {
|
||||
try {
|
||||
final photos = await getAsetPhotos(aset.id);
|
||||
if (photos.isNotEmpty &&
|
||||
(aset.imageUrl == null || aset.imageUrl!.isEmpty)) {
|
||||
aset.imageUrl = photos.first.fotoAset;
|
||||
if (photos.isNotEmpty) {
|
||||
// Clear existing images
|
||||
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) {
|
||||
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
|
||||
Future<List<Map<String, dynamic>>> getAsetBookings(
|
||||
String asetId,
|
||||
@ -1061,7 +1468,9 @@ class AsetProvider extends GetxService {
|
||||
.order('created_at');
|
||||
|
||||
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 [];
|
||||
} catch (e) {
|
||||
@ -1095,16 +1504,22 @@ class AsetProvider extends GetxService {
|
||||
}
|
||||
|
||||
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 [];
|
||||
}
|
||||
|
||||
debugPrint('✅ [SUCCESS] Found ${response.length} items in paket_item');
|
||||
debugPrint(
|
||||
'✅ [SUCCESS] Found [1m${response.length}[0m items in paket_item',
|
||||
);
|
||||
|
||||
final List<Map<String, dynamic>> enrichedItems = [];
|
||||
|
||||
// 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) {
|
||||
final String? asetId = item['aset_id']?.toString();
|
||||
@ -1123,13 +1538,16 @@ class AsetProvider extends GetxService {
|
||||
try {
|
||||
// 1. Get asset name from aset table
|
||||
debugPrint(' - Querying aset table for id: $asetId');
|
||||
final asetResponse = await client
|
||||
.from('aset')
|
||||
.select('id, nama, deskripsi')
|
||||
.eq('id', asetId)
|
||||
.maybeSingle();
|
||||
final asetResponse =
|
||||
await client
|
||||
.from('aset')
|
||||
.select('id, nama, deskripsi')
|
||||
.eq('id', asetId)
|
||||
.maybeSingle();
|
||||
|
||||
debugPrint(' - Aset response: ${asetResponse?.toString() ?? 'null'}');
|
||||
debugPrint(
|
||||
' - Aset response: ${asetResponse?.toString() ?? 'null'}',
|
||||
);
|
||||
|
||||
if (asetResponse == null) {
|
||||
debugPrint('⚠️ [WARNING] No asset found with id: $asetId');
|
||||
@ -1139,7 +1557,7 @@ class AsetProvider extends GetxService {
|
||||
'nama_aset': 'Item tidak diketahui',
|
||||
'foto_aset': '',
|
||||
'semua_foto': <String>[],
|
||||
'error': 'Asset not found'
|
||||
'error': 'Asset not found',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@ -1173,15 +1591,18 @@ class AsetProvider extends GetxService {
|
||||
final enrichedItem = {
|
||||
'aset_id': asetId,
|
||||
'kuantitas': kuantitas,
|
||||
'nama_aset': asetResponse['nama']?.toString() ?? 'Nama tidak tersedia',
|
||||
'nama_aset':
|
||||
asetResponse['nama']?.toString() ?? 'Nama tidak tersedia',
|
||||
'foto_aset': fotoUtama,
|
||||
'semua_foto': semuaFoto,
|
||||
'debug': {
|
||||
'aset_query': asetResponse,
|
||||
'foto_count': semuaFoto.length
|
||||
}
|
||||
'foto_count': semuaFoto.length,
|
||||
},
|
||||
};
|
||||
|
||||
debugPrint('✅ [ENRICHED ITEM] $enrichedItem');
|
||||
|
||||
enrichedItems.add(enrichedItem);
|
||||
|
||||
// Debug log
|
||||
@ -1193,7 +1614,6 @@ class AsetProvider extends GetxService {
|
||||
if (semuaFoto.isNotEmpty) {
|
||||
debugPrint(' - Foto Utama: ${semuaFoto.first}');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error processing asset $asetId: $e');
|
||||
// Still add the basic item even if we couldn't fetch additional details
|
||||
@ -1207,9 +1627,13 @@ 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;
|
||||
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ Error getting package items for paket $paketId: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
@ -1221,10 +1645,9 @@ class AsetProvider extends GetxService {
|
||||
Future<List<Map<String, dynamic>>> getBankAccounts() async {
|
||||
try {
|
||||
final response = await client
|
||||
.from('bank_accounts')
|
||||
.from('akun_bank')
|
||||
.select('*')
|
||||
.eq('is_active', true)
|
||||
.order('bank_name');
|
||||
.order('nama_bank');
|
||||
|
||||
if (response != null && response.isNotEmpty) {
|
||||
return List<Map<String, dynamic>>.from(response);
|
||||
@ -1235,4 +1658,325 @@ class AsetProvider extends GetxService {
|
||||
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
10
lib/app/main.dart
Normal file
@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||
import 'package:bumrent_app/main.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
Get.put(AsetProvider());
|
||||
runApp(const MyApp());
|
||||
}
|
@ -8,6 +8,10 @@ class AuthController extends GetxController {
|
||||
|
||||
final emailController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final nameController = TextEditingController();
|
||||
final confirmPasswordController = TextEditingController();
|
||||
final RxBool isConfirmPasswordVisible = false.obs;
|
||||
|
||||
// Form fields for registration
|
||||
final RxString email = ''.obs;
|
||||
@ -15,6 +19,7 @@ class AuthController extends GetxController {
|
||||
final RxString nik = ''.obs;
|
||||
final RxString phoneNumber = ''.obs;
|
||||
final RxString selectedRole = 'WARGA'.obs; // Default role
|
||||
final RxString alamatLengkap = ''.obs;
|
||||
|
||||
// Form status
|
||||
final RxBool isLoading = false.obs;
|
||||
@ -28,6 +33,10 @@ class AuthController extends GetxController {
|
||||
isPasswordVisible.value = !isPasswordVisible.value;
|
||||
}
|
||||
|
||||
void toggleConfirmPasswordVisibility() {
|
||||
isConfirmPasswordVisible.value = !isConfirmPasswordVisible.value;
|
||||
}
|
||||
|
||||
// Change role selection
|
||||
void setRole(String? role) {
|
||||
if (role != null) {
|
||||
@ -172,6 +181,8 @@ class AuthController extends GetxController {
|
||||
void onClose() {
|
||||
emailController.dispose();
|
||||
passwordController.dispose();
|
||||
nameController.dispose();
|
||||
confirmPasswordController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@ -181,7 +192,8 @@ class AuthController extends GetxController {
|
||||
if (email.value.isEmpty ||
|
||||
password.value.isEmpty ||
|
||||
nik.value.isEmpty ||
|
||||
phoneNumber.value.isEmpty) {
|
||||
phoneNumber.value.isEmpty ||
|
||||
alamatLengkap.value.isEmpty) {
|
||||
errorMessage.value = 'Semua field harus diisi';
|
||||
return;
|
||||
}
|
||||
@ -222,6 +234,7 @@ class AuthController extends GetxController {
|
||||
data: {
|
||||
'nik': nik.value.trim(),
|
||||
'phone_number': phoneNumber.value.trim(),
|
||||
'alamat_lengkap': alamatLengkap.value.trim(),
|
||||
'role': selectedRole.value,
|
||||
},
|
||||
);
|
||||
|
@ -26,12 +26,8 @@ class ForgotPasswordView extends GetView<AuthController> {
|
||||
Opacity(
|
||||
opacity: 0.03,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/pattern.png'),
|
||||
repeat: ImageRepeat.repeat,
|
||||
scale: 4.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[50], // Temporary solid color
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -30,12 +30,8 @@ class LoginView extends GetView<AuthController> {
|
||||
Opacity(
|
||||
opacity: 0.03,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/pattern.png'),
|
||||
repeat: ImageRepeat.repeat,
|
||||
scale: 4.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[50], // Temporary solid color
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -89,7 +85,6 @@ class LoginView extends GetView<AuthController> {
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 40),
|
||||
_buildLoginCard(),
|
||||
const SizedBox(height: 24),
|
||||
_buildRegisterLink(),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
@ -161,7 +156,7 @@ class LoginView extends GetView<AuthController> {
|
||||
prefixIcon: Icons.email_outlined,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Password field
|
||||
_buildInputLabel('Password'),
|
||||
@ -204,7 +199,6 @@ class LoginView extends GetView<AuthController> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Login button
|
||||
Obx(
|
||||
|
@ -187,7 +187,7 @@ class RegistrationView extends GetView<AuthController> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
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(
|
||||
fontSize: 13,
|
||||
color: AppColors.textPrimary,
|
||||
@ -203,121 +203,32 @@ class RegistrationView extends GetView<AuthController> {
|
||||
}
|
||||
|
||||
Widget _buildRegistrationForm() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Email Input
|
||||
_buildInputLabel('Email'),
|
||||
const SizedBox(height: 8),
|
||||
_buildEmailField(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Password Input
|
||||
_buildInputLabel('Password'),
|
||||
const SizedBox(height: 8),
|
||||
_buildPasswordField(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// NIK Input
|
||||
_buildInputLabel('NIK'),
|
||||
const SizedBox(height: 8),
|
||||
_buildNikField(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Phone Number Input
|
||||
_buildInputLabel('No. Hp'),
|
||||
const SizedBox(height: 8),
|
||||
_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(),
|
||||
),
|
||||
],
|
||||
return Form(
|
||||
key: controller.formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildInputLabel('Email'),
|
||||
_buildEmailField(),
|
||||
const SizedBox(height: 16),
|
||||
_buildInputLabel('Password'),
|
||||
_buildPasswordField(),
|
||||
const SizedBox(height: 16),
|
||||
_buildInputLabel('Konfirmasi Password'),
|
||||
_buildConfirmPasswordField(),
|
||||
const SizedBox(height: 16),
|
||||
_buildInputLabel('Nama Lengkap'),
|
||||
_buildNameField(),
|
||||
const SizedBox(height: 16),
|
||||
_buildInputLabel('No HP'),
|
||||
_buildPhoneField(),
|
||||
const SizedBox(height: 16),
|
||||
_buildInputLabel('Alamat Lengkap'),
|
||||
_buildAlamatField(),
|
||||
const SizedBox(height: 16),
|
||||
// Removed: NIK, No HP, and Dropdown Daftar Sebagai
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -415,78 +326,101 @@ class RegistrationView extends GetView<AuthController> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNikField() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
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),
|
||||
Widget _buildConfirmPasswordField() {
|
||||
return Obx(
|
||||
() => TextFormField(
|
||||
controller: controller.confirmPasswordController,
|
||||
obscureText: !controller.isConfirmPasswordVisible.value,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Masukkan NIK anda',
|
||||
hintStyle: TextStyle(color: AppColors.textLight),
|
||||
prefixIcon: Icon(
|
||||
Icons.credit_card_outlined,
|
||||
color: AppColors.primary,
|
||||
hintText: 'Masukkan ulang password anda',
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
controller.isConfirmPasswordVisible.value
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: controller.toggleConfirmPasswordVisibility,
|
||||
),
|
||||
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),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
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() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 8,
|
||||
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),
|
||||
),
|
||||
Widget _buildNameField() {
|
||||
return TextFormField(
|
||||
controller: controller.nameController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Masukkan nama lengkap anda',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
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;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_aset_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
|
||||
class PetugasAsetBinding extends Bindings {
|
||||
@override
|
||||
@ -10,6 +11,7 @@ class PetugasAsetBinding extends Bindings {
|
||||
Get.put(PetugasBumdesDashboardController(), permanent: true);
|
||||
}
|
||||
|
||||
Get.lazyPut<AsetProvider>(() => AsetProvider());
|
||||
Get.lazyPut<PetugasAsetController>(() => PetugasAsetController());
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_sewa_controller.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
|
||||
class PetugasDetailSewaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Ensure AsetProvider is registered
|
||||
if (!Get.isRegistered<AsetProvider>()) {
|
||||
Get.put(AsetProvider(), permanent: true);
|
||||
}
|
||||
// Memastikan controller sudah tersedia
|
||||
Get.lazyPut<PetugasSewaController>(
|
||||
() => PetugasSewaController(),
|
||||
|
@ -1,15 +1,25 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||
import '../controllers/petugas_paket_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
|
||||
class PetugasPaketBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Register AsetProvider first
|
||||
if (!Get.isRegistered<AsetProvider>()) {
|
||||
Get.put(AsetProvider(), permanent: true);
|
||||
}
|
||||
|
||||
// Ensure dashboard controller is registered
|
||||
if (!Get.isRegistered<PetugasBumdesDashboardController>()) {
|
||||
Get.put(PetugasBumdesDashboardController(), permanent: true);
|
||||
}
|
||||
|
||||
Get.lazyPut<PetugasPaketController>(() => PetugasPaketController());
|
||||
// Register the controller
|
||||
Get.lazyPut<PetugasPaketController>(
|
||||
() => PetugasPaketController(),
|
||||
fenix: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_sewa_controller.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
|
||||
class PetugasSewaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Ensure AsetProvider is registered
|
||||
if (!Get.isRegistered<AsetProvider>()) {
|
||||
Get.put(AsetProvider(), permanent: true);
|
||||
}
|
||||
Get.lazyPut<PetugasSewaController>(() => PetugasSewaController());
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,11 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
import '../../../data/models/aset_model.dart';
|
||||
|
||||
class PetugasAsetController extends GetxController {
|
||||
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
||||
// Observable lists for asset data
|
||||
final asetList = <Map<String, dynamic>>[].obs;
|
||||
final filteredAsetList = <Map<String, dynamic>>[].obs;
|
||||
@ -27,95 +32,100 @@ class PetugasAsetController extends GetxController {
|
||||
loadAsetData();
|
||||
}
|
||||
|
||||
// Load sample asset data (would be replaced with API call in production)
|
||||
// Load asset data from AsetProvider
|
||||
Future<void> loadAsetData() async {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// Simulate API call with a delay
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
isLoading.value = true;
|
||||
debugPrint('PetugasAsetController: Starting to load asset data...');
|
||||
|
||||
// Sample assets data
|
||||
final sampleData = [
|
||||
{
|
||||
'id': '1',
|
||||
'nama': 'Meja Rapat',
|
||||
'kategori': 'Furniture',
|
||||
'jenis': 'Sewa', // Added jenis field
|
||||
'harga': 50000,
|
||||
'satuan': 'per hari',
|
||||
'stok': 10,
|
||||
'deskripsi':
|
||||
'Meja rapat kayu jati ukuran besar untuk acara pertemuan',
|
||||
'gambar': 'https://example.com/meja.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
{
|
||||
'id': '2',
|
||||
'nama': 'Kursi Taman',
|
||||
'kategori': 'Furniture',
|
||||
'jenis': 'Sewa', // Added jenis field
|
||||
'harga': 10000,
|
||||
'satuan': 'per hari',
|
||||
'stok': 50,
|
||||
'deskripsi': 'Kursi taman plastik yang nyaman untuk acara outdoor',
|
||||
'gambar': 'https://example.com/kursi.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
{
|
||||
'id': '3',
|
||||
'nama': 'Proyektor',
|
||||
'kategori': 'Elektronik',
|
||||
'jenis': 'Sewa', // Added jenis field
|
||||
'harga': 100000,
|
||||
'satuan': 'per hari',
|
||||
'stok': 5,
|
||||
'deskripsi': 'Proyektor HD dengan brightness tinggi',
|
||||
'gambar': 'https://example.com/proyektor.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
{
|
||||
'id': '4',
|
||||
'nama': 'Sound System',
|
||||
'kategori': 'Elektronik',
|
||||
'jenis': 'Langganan', // Added jenis field
|
||||
'harga': 200000,
|
||||
'satuan': 'per bulan',
|
||||
'stok': 3,
|
||||
'deskripsi': 'Sound system lengkap dengan speaker dan mixer',
|
||||
'gambar': 'https://example.com/sound.jpg',
|
||||
'tersedia': false,
|
||||
},
|
||||
{
|
||||
'id': '5',
|
||||
'nama': 'Mobil Pick Up',
|
||||
'kategori': 'Kendaraan',
|
||||
'jenis': 'Langganan', // Added jenis field
|
||||
'harga': 250000,
|
||||
'satuan': 'per bulan',
|
||||
'stok': 2,
|
||||
'deskripsi': 'Mobil pick up untuk mengangkut barang',
|
||||
'gambar': 'https://example.com/pickup.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
{
|
||||
'id': '6',
|
||||
'nama': 'Internet Fiber',
|
||||
'kategori': 'Elektronik',
|
||||
'jenis': 'Langganan', // Added jenis field
|
||||
'harga': 350000,
|
||||
'satuan': 'per bulan',
|
||||
'stok': 15,
|
||||
'deskripsi': 'Paket internet fiber 100Mbps untuk kantor',
|
||||
'gambar': 'https://example.com/internet.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
];
|
||||
// Fetch data using AsetProvider
|
||||
final asetData = await _asetProvider.getSewaAsets();
|
||||
debugPrint(
|
||||
'PetugasAsetController: Fetched ${asetData.length} assets from Supabase',
|
||||
);
|
||||
|
||||
asetList.assignAll(sampleData);
|
||||
applyFilters(); // Apply default filters
|
||||
} catch (e) {
|
||||
print('Error loading asset data: $e');
|
||||
if (asetData.isEmpty) {
|
||||
debugPrint('PetugasAsetController: No assets found in Supabase');
|
||||
}
|
||||
|
||||
final List<Map<String, dynamic>> mappedAsets = [];
|
||||
int index = 0; // Initialize index counter
|
||||
for (var aset in asetData) {
|
||||
String displayKategori = 'Umum'; // Placeholder for descriptive category
|
||||
// Attempt to derive a more specific category from description if needed, or add to AsetModel
|
||||
if (aset.deskripsi.toLowerCase().contains('meja') ||
|
||||
aset.deskripsi.toLowerCase().contains('kursi')) {
|
||||
displayKategori = 'Furniture';
|
||||
} else if (aset.deskripsi.toLowerCase().contains('proyektor') ||
|
||||
aset.deskripsi.toLowerCase().contains('sound') ||
|
||||
aset.deskripsi.toLowerCase().contains('internet')) {
|
||||
displayKategori = 'Elektronik';
|
||||
} else if (aset.deskripsi.toLowerCase().contains('mobil') ||
|
||||
aset.deskripsi.toLowerCase().contains('kendaraan')) {
|
||||
displayKategori = 'Kendaraan';
|
||||
}
|
||||
|
||||
final map = {
|
||||
'id': aset.id,
|
||||
'nama': aset.nama,
|
||||
'deskripsi': aset.deskripsi,
|
||||
'harga':
|
||||
aset.satuanWaktuSewa.isNotEmpty
|
||||
? aset.satuanWaktuSewa.first['harga']
|
||||
: 0,
|
||||
'status': aset.status,
|
||||
'kategori': displayKategori,
|
||||
'jenis': aset.jenis ?? 'Sewa', // Add this line with default value
|
||||
'imageUrl': aset.imageUrl ?? 'https://via.placeholder.com/150',
|
||||
'satuan_waktu':
|
||||
aset.satuanWaktuSewa.isNotEmpty
|
||||
? aset.satuanWaktuSewa.first['nama_satuan_waktu'] ?? 'Hari'
|
||||
: 'Hari',
|
||||
'satuanWaktuSewa': aset.satuanWaktuSewa.toList(),
|
||||
};
|
||||
|
||||
debugPrint('Mapped asset #$index: $map');
|
||||
mappedAsets.add(map);
|
||||
index++;
|
||||
debugPrint('Deskripsi: ${aset.deskripsi}');
|
||||
debugPrint('Kategori (from AsetModel): ${aset.kategori}');
|
||||
debugPrint('Status: ${aset.status}');
|
||||
debugPrint('Mapped Kategori for Petugas View: ${map['kategori']}');
|
||||
debugPrint('Mapped Jenis for Petugas View: ${map['jenis']}');
|
||||
debugPrint('--------------------------------');
|
||||
}
|
||||
|
||||
// Populate asetList with fetched data and apply filters
|
||||
debugPrint(
|
||||
'PetugasAsetController: Mapped ${mappedAsets.length} assets for display',
|
||||
);
|
||||
asetList.assignAll(mappedAsets); // Make data available to UI
|
||||
debugPrint(
|
||||
'PetugasAsetController: asetList now has ${asetList.length} items',
|
||||
);
|
||||
|
||||
applyFilters(); // Apply initial filters
|
||||
debugPrint(
|
||||
'PetugasAsetController: Applied filters. filteredAsetList has ${filteredAsetList.length} items',
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
'PetugasAsetController: Data loading complete. Asset list populated and filters applied.',
|
||||
);
|
||||
debugPrint(
|
||||
'PetugasAsetController: First asset name: ${mappedAsets.isNotEmpty ? mappedAsets[0]['nama'] : 'No assets'}',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('PetugasAsetController: Error loading asset data: $e');
|
||||
debugPrint('PetugasAsetController: StackTrace: $stackTrace');
|
||||
// Optionally, show a snackbar or error message to the user
|
||||
Get.snackbar(
|
||||
'Error Memuat Data',
|
||||
'Gagal mengambil data aset dari server. Silakan coba lagi nanti.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
@ -170,8 +180,10 @@ class PetugasAsetController extends GetxController {
|
||||
}
|
||||
|
||||
// Change tab (Sewa or Langganan)
|
||||
void changeTab(int index) {
|
||||
Future<void> changeTab(int index) async {
|
||||
selectedTabIndex.value = index;
|
||||
// Reload data when changing tabs to ensure we have the correct data for the selected tab
|
||||
await loadAsetData();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,10 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../../../data/providers/auth_provider.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 {
|
||||
AuthProvider? _authProvider;
|
||||
@ -8,6 +12,8 @@ class PetugasBumdesDashboardController extends GetxController {
|
||||
// Reactive variables
|
||||
final userEmail = ''.obs;
|
||||
final currentTabIndex = 0.obs;
|
||||
final avatarUrl = ''.obs;
|
||||
final userName = ''.obs;
|
||||
|
||||
// Revenue Statistics
|
||||
final totalPendapatanBulanIni = 'Rp 8.500.000'.obs;
|
||||
@ -20,7 +26,7 @@ class PetugasBumdesDashboardController extends GetxController {
|
||||
final persentaseSewa = 100.obs;
|
||||
|
||||
// Revenue Trends (last 6 months)
|
||||
final trendPendapatan = [4.2, 5.1, 4.8, 6.2, 7.2, 8.5].obs; // in millions
|
||||
final trendPendapatan = <double>[].obs; // 6 bulan terakhir
|
||||
|
||||
// Status Counters for Sewa Aset
|
||||
final terlaksanaCount = 5.obs;
|
||||
@ -43,42 +49,128 @@ class PetugasBumdesDashboardController extends GetxController {
|
||||
final tagihanAktifCountSewa = 7.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
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
try {
|
||||
_authProvider = Get.find<AuthProvider>();
|
||||
userEmail.value = _authProvider?.currentUser?.email ?? 'Tidak ada email';
|
||||
fetchPetugasAvatar();
|
||||
fetchPetugasName();
|
||||
} catch (e) {
|
||||
print('Error finding AuthProvider: $e');
|
||||
userEmail.value = 'Tidak ada email';
|
||||
}
|
||||
|
||||
// In a real app, these counts would be fetched from backend
|
||||
// loadStatusCounts();
|
||||
print('✅ PetugasBumdesDashboardController initialized successfully');
|
||||
print('\u2705 PetugasBumdesDashboardController initialized successfully');
|
||||
countSewaByStatus();
|
||||
fetchPembayaranStats();
|
||||
}
|
||||
|
||||
// Method to load status counts from backend
|
||||
// Future<void> loadStatusCounts() async {
|
||||
// try {
|
||||
// final response = await _asetProvider.getSewaStatusCounts();
|
||||
// if (response != null) {
|
||||
// terlaksanaCount.value = response['terlaksana'] ?? 0;
|
||||
// dijadwalkanCount.value = response['dijadwalkan'] ?? 0;
|
||||
// aktifCount.value = response['aktif'] ?? 0;
|
||||
// dibatalkanCount.value = response['dibatalkan'] ?? 0;
|
||||
// menungguPembayaranCount.value = response['menunggu_pembayaran'] ?? 0;
|
||||
// periksaPembayaranCount.value = response['periksa_pembayaran'] ?? 0;
|
||||
// diterimaCount.value = response['diterima'] ?? 0;
|
||||
// pembayaranDendaCount.value = response['pembayaran_denda'] ?? 0;
|
||||
// periksaPembayaranDendaCount.value = response['periksa_pembayaran_denda'] ?? 0;
|
||||
// selesaiCount.value = response['selesai'] ?? 0;
|
||||
// }
|
||||
// } catch (e) {
|
||||
// print('Error loading status counts: $e');
|
||||
// }
|
||||
// }
|
||||
Future<void> countSewaByStatus() async {
|
||||
try {
|
||||
final data = await SewaService().fetchAllSewa();
|
||||
menungguPembayaranCount.value =
|
||||
data.where((s) => s.status == 'MENUNGGU PEMBAYARAN').length;
|
||||
periksaPembayaranCount.value =
|
||||
data.where((s) => s.status == 'PERIKSA PEMBAYARAN').length;
|
||||
diterimaCount.value = data.where((s) => s.status == 'DITERIMA').length;
|
||||
pembayaranDendaCount.value =
|
||||
data.where((s) => s.status == 'PEMBAYARAN DENDA').length;
|
||||
periksaPembayaranDendaCount.value =
|
||||
data.where((s) => s.status == 'PERIKSA PEMBAYARAN DENDA').length;
|
||||
selesaiCount.value = data.where((s) => s.status == 'SELESAI').length;
|
||||
print(
|
||||
'Count for MENUNGGU PEMBAYARAN: \\${menungguPembayaranCount.value}',
|
||||
);
|
||||
print('Count for PERIKSA PEMBAYARAN: \\${periksaPembayaranCount.value}');
|
||||
print('Count for DITERIMA: \\${diterimaCount.value}');
|
||||
print('Count for PEMBAYARAN DENDA: \\${pembayaranDendaCount.value}');
|
||||
print(
|
||||
'Count for PERIKSA PEMBAYARAN DENDA: \\${periksaPembayaranDendaCount.value}',
|
||||
);
|
||||
print('Count for SELESAI: \\${selesaiCount.value}');
|
||||
} catch (e) {
|
||||
print('Error counting sewa by status: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchPembayaranStats() async {
|
||||
isStatsLoading.value = true;
|
||||
try {
|
||||
final stats = await PembayaranService().fetchStats();
|
||||
pembayaranStats.value = stats;
|
||||
// Set trendPendapatan from stats['trendPerMonth'] if available
|
||||
if (stats['trendPerMonth'] != null) {
|
||||
trendPendapatan.value = List<double>.from(stats['trendPerMonth']);
|
||||
}
|
||||
print('Pembayaran stats: $stats');
|
||||
} catch (e, st) {
|
||||
print('Error fetching pembayaran stats: $e\n$st');
|
||||
pembayaranStats.value = {};
|
||||
trendPendapatan.value = [];
|
||||
}
|
||||
isStatsLoading.value = false;
|
||||
}
|
||||
|
||||
Future<void> fetchPetugasAvatar() async {
|
||||
try {
|
||||
final userId = _authProvider?.getCurrentUserId();
|
||||
if (userId == null) return;
|
||||
final client = _authProvider!.client;
|
||||
final data =
|
||||
await client
|
||||
.from('petugas_bumdes')
|
||||
.select('avatar')
|
||||
.eq('id', userId)
|
||||
.maybeSingle();
|
||||
if (data != null &&
|
||||
data['avatar'] != null &&
|
||||
data['avatar'].toString().isNotEmpty) {
|
||||
avatarUrl.value = data['avatar'].toString();
|
||||
} else {
|
||||
avatarUrl.value = '';
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching petugas avatar: $e');
|
||||
avatarUrl.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchPetugasName() async {
|
||||
try {
|
||||
final userId = _authProvider?.getCurrentUserId();
|
||||
if (userId == null) return;
|
||||
final client = _authProvider!.client;
|
||||
final data =
|
||||
await client
|
||||
.from('petugas_bumdes')
|
||||
.select('nama')
|
||||
.eq('id', userId)
|
||||
.maybeSingle();
|
||||
if (data != null &&
|
||||
data['nama'] != null &&
|
||||
data['nama'].toString().isNotEmpty) {
|
||||
userName.value = data['nama'].toString();
|
||||
} else {
|
||||
userName.value = '';
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching petugas name: $e');
|
||||
userName.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
void changeTab(int index) {
|
||||
try {
|
||||
|
@ -1,24 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/intl.dart' show NumberFormat;
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:bumrent_app/app/data/models/paket_model.dart';
|
||||
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||
|
||||
class PetugasPaketController extends GetxController {
|
||||
final isLoading = false.obs;
|
||||
final searchQuery = ''.obs;
|
||||
final selectedCategory = 'Semua'.obs;
|
||||
final sortBy = 'Terbaru'.obs;
|
||||
// Dependencies
|
||||
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
||||
|
||||
// Kategori untuk filter
|
||||
final categories = <String>[
|
||||
'Semua',
|
||||
'Pesta',
|
||||
'Rapat',
|
||||
'Olahraga',
|
||||
'Pernikahan',
|
||||
'Lainnya',
|
||||
];
|
||||
// State
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxString searchQuery = ''.obs;
|
||||
final RxString selectedCategory = 'Semua'.obs;
|
||||
final RxString sortBy = 'Terbaru'.obs;
|
||||
final RxList<PaketModel> packages = <PaketModel>[].obs;
|
||||
final RxList<PaketModel> filteredPackages = <PaketModel>[].obs;
|
||||
|
||||
// Opsi pengurutan
|
||||
final sortOptions = <String>[
|
||||
// Sort options for the dropdown
|
||||
final List<String> sortOptions = [
|
||||
'Terbaru',
|
||||
'Terlama',
|
||||
'Harga Tertinggi',
|
||||
@ -27,171 +27,217 @@ class PetugasPaketController extends GetxController {
|
||||
'Nama Z-A',
|
||||
];
|
||||
|
||||
// Data dummy paket
|
||||
final paketList = <Map<String, dynamic>>[].obs;
|
||||
final filteredPaketList = <Map<String, dynamic>>[].obs;
|
||||
// For backward compatibility
|
||||
final RxList<Map<String, dynamic>> paketList = <Map<String, dynamic>>[].obs;
|
||||
final RxList<Map<String, dynamic>> filteredPaketList = <Map<String, dynamic>>[].obs;
|
||||
|
||||
// Logger
|
||||
late final Logger _logger;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadPaketData();
|
||||
}
|
||||
|
||||
// Format harga ke Rupiah
|
||||
String formatPrice(int price) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
// Initialize logger
|
||||
_logger = Logger(
|
||||
printer: PrettyPrinter(
|
||||
methodCount: 0,
|
||||
errorMethodCount: 5,
|
||||
colors: true,
|
||||
printEmojis: true,
|
||||
),
|
||||
);
|
||||
return formatter.format(price);
|
||||
|
||||
// Load initial data
|
||||
fetchPackages();
|
||||
}
|
||||
|
||||
// Load data paket dummy
|
||||
/// Fetch packages from the API
|
||||
Future<void> fetchPackages() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
_logger.i('🔄 [fetchPackages] Fetching packages...');
|
||||
|
||||
final result = await _asetProvider.getAllPaket();
|
||||
|
||||
if (result.isEmpty) {
|
||||
_logger.w('ℹ️ [fetchPackages] No packages found');
|
||||
packages.clear();
|
||||
filteredPackages.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
packages.assignAll(result);
|
||||
filteredPackages.assignAll(result);
|
||||
|
||||
// Update legacy list for backward compatibility
|
||||
_updateLegacyPaketList();
|
||||
|
||||
_logger.i('✅ [fetchPackages] Successfully loaded ${result.length} packages');
|
||||
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e('❌ [fetchPackages] Error fetching packages',
|
||||
error: e,
|
||||
stackTrace: stackTrace);
|
||||
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memuat data paket. Silakan coba lagi.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update legacy paketList for backward compatibility
|
||||
void _updateLegacyPaketList() {
|
||||
try {
|
||||
_logger.d('🔄 [_updateLegacyPaketList] Updating legacy paketList...');
|
||||
|
||||
final List<Map<String, dynamic>> legacyList = packages.map((pkg) {
|
||||
return {
|
||||
'id': pkg.id,
|
||||
'nama': pkg.nama,
|
||||
'deskripsi': pkg.deskripsi,
|
||||
'harga': pkg.harga,
|
||||
'kuantitas': pkg.kuantitas,
|
||||
'status': pkg.status, // Add status to legacy mapping
|
||||
'foto': pkg.foto,
|
||||
'foto_paket': pkg.foto_paket,
|
||||
'images': pkg.images,
|
||||
'satuanWaktuSewa': pkg.satuanWaktuSewa,
|
||||
'created_at': pkg.createdAt,
|
||||
'updated_at': pkg.updatedAt,
|
||||
};
|
||||
}).toList();
|
||||
|
||||
paketList.assignAll(legacyList);
|
||||
filteredPaketList.assignAll(legacyList);
|
||||
|
||||
_logger.d('✅ [_updateLegacyPaketList] Updated ${legacyList.length} packages');
|
||||
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e('❌ [_updateLegacyPaketList] Error updating legacy list',
|
||||
error: e,
|
||||
stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// For backward compatibility
|
||||
Future<void> loadPaketData() async {
|
||||
isLoading.value = true;
|
||||
await Future.delayed(const Duration(milliseconds: 800)); // Simulasi loading
|
||||
|
||||
paketList.value = [
|
||||
{
|
||||
'id': '1',
|
||||
'nama': 'Paket Pesta Ulang Tahun',
|
||||
'kategori': 'Pesta',
|
||||
'harga': 500000,
|
||||
'deskripsi':
|
||||
'Paket lengkap untuk acara ulang tahun. Termasuk 5 meja, 20 kursi, backdrop, dan sound system.',
|
||||
'tersedia': true,
|
||||
'created_at': '2023-08-10',
|
||||
'items': [
|
||||
{'nama': 'Meja Panjang', 'jumlah': 5},
|
||||
{'nama': 'Kursi Plastik', 'jumlah': 20},
|
||||
{'nama': 'Sound System', 'jumlah': 1},
|
||||
{'nama': 'Backdrop', 'jumlah': 1},
|
||||
],
|
||||
'gambar': 'https://example.com/images/paket_ultah.jpg',
|
||||
},
|
||||
{
|
||||
'id': '2',
|
||||
'nama': 'Paket Rapat Sedang',
|
||||
'kategori': 'Rapat',
|
||||
'harga': 300000,
|
||||
'deskripsi':
|
||||
'Paket untuk rapat sedang. Termasuk 1 meja rapat besar, 10 kursi, proyektor, dan screen.',
|
||||
'tersedia': true,
|
||||
'created_at': '2023-09-05',
|
||||
'items': [
|
||||
{'nama': 'Meja Rapat', 'jumlah': 1},
|
||||
{'nama': 'Kursi Kantor', 'jumlah': 10},
|
||||
{'nama': 'Proyektor', 'jumlah': 1},
|
||||
{'nama': 'Screen', 'jumlah': 1},
|
||||
],
|
||||
'gambar': 'https://example.com/images/paket_rapat.jpg',
|
||||
},
|
||||
{
|
||||
'id': '3',
|
||||
'nama': 'Paket Pesta Pernikahan',
|
||||
'kategori': 'Pernikahan',
|
||||
'harga': 1500000,
|
||||
'deskripsi':
|
||||
'Paket lengkap untuk acara pernikahan. Termasuk 20 meja, 100 kursi, sound system, dekorasi, dan tenda.',
|
||||
'tersedia': true,
|
||||
'created_at': '2023-10-12',
|
||||
'items': [
|
||||
{'nama': 'Meja Bundar', 'jumlah': 20},
|
||||
{'nama': 'Kursi Tamu', 'jumlah': 100},
|
||||
{'nama': 'Sound System Besar', 'jumlah': 1},
|
||||
{'nama': 'Tenda 10x10', 'jumlah': 2},
|
||||
{'nama': 'Set Dekorasi Pengantin', 'jumlah': 1},
|
||||
],
|
||||
'gambar': 'https://example.com/images/paket_nikah.jpg',
|
||||
},
|
||||
{
|
||||
'id': '4',
|
||||
'nama': 'Paket Olahraga Voli',
|
||||
'kategori': 'Olahraga',
|
||||
'harga': 200000,
|
||||
'deskripsi':
|
||||
'Paket perlengkapan untuk turnamen voli. Termasuk net, bola, dan tiang voli.',
|
||||
'tersedia': false,
|
||||
'created_at': '2023-07-22',
|
||||
'items': [
|
||||
{'nama': 'Net Voli', 'jumlah': 1},
|
||||
{'nama': 'Bola Voli', 'jumlah': 3},
|
||||
{'nama': 'Tiang Voli', 'jumlah': 2},
|
||||
],
|
||||
'gambar': 'https://example.com/images/paket_voli.jpg',
|
||||
},
|
||||
{
|
||||
'id': '5',
|
||||
'nama': 'Paket Pesta Anak',
|
||||
'kategori': 'Pesta',
|
||||
'harga': 350000,
|
||||
'deskripsi':
|
||||
'Paket untuk pesta ulang tahun anak-anak. Termasuk 3 meja, 15 kursi, dekorasi tema, dan sound system kecil.',
|
||||
'tersedia': true,
|
||||
'created_at': '2023-11-01',
|
||||
'items': [
|
||||
{'nama': 'Meja Anak', 'jumlah': 3},
|
||||
{'nama': 'Kursi Anak', 'jumlah': 15},
|
||||
{'nama': 'Set Dekorasi Tema', 'jumlah': 1},
|
||||
{'nama': 'Sound System Kecil', 'jumlah': 1},
|
||||
],
|
||||
'gambar': 'https://example.com/images/paket_anak.jpg',
|
||||
},
|
||||
];
|
||||
|
||||
filterPaket();
|
||||
isLoading.value = false;
|
||||
_logger.d('ℹ️ [loadPaketData] Using fetchPackages() instead');
|
||||
await fetchPackages();
|
||||
}
|
||||
|
||||
// Filter paket berdasarkan search query dan kategori
|
||||
/// Filter packages based on search query and category
|
||||
void filterPaket() {
|
||||
filteredPaketList.value =
|
||||
paketList.where((paket) {
|
||||
final matchesQuery =
|
||||
paket['nama'].toString().toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
) ||
|
||||
paket['deskripsi'].toString().toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
);
|
||||
try {
|
||||
_logger.d('🔄 [filterPaket] Filtering packages...');
|
||||
|
||||
final matchesCategory =
|
||||
selectedCategory.value == 'Semua' ||
|
||||
paket['kategori'] == selectedCategory.value;
|
||||
if (searchQuery.value.isEmpty && selectedCategory.value == 'Semua') {
|
||||
filteredPackages.value = List.from(packages);
|
||||
filteredPaketList.value = List.from(paketList);
|
||||
} else {
|
||||
// Filter new packages
|
||||
filteredPackages.value = packages.where((paket) {
|
||||
final matchesSearch = searchQuery.value.isEmpty ||
|
||||
paket.nama.toLowerCase().contains(searchQuery.value.toLowerCase());
|
||||
|
||||
return matchesQuery && matchesCategory;
|
||||
// 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();
|
||||
|
||||
// Sort the filtered list
|
||||
sortFilteredList();
|
||||
// Also update legacy list for backward compatibility
|
||||
filteredPaketList.value = paketList.where((paket) {
|
||||
final matchesSearch = searchQuery.value.isEmpty ||
|
||||
(paket['nama']?.toString() ?? '').toLowerCase()
|
||||
.contains(searchQuery.value.toLowerCase());
|
||||
|
||||
// For legacy support, check if category exists
|
||||
final matchesCategory = selectedCategory.value == 'Semua' ||
|
||||
(paket['kategori']?.toString() ?? '') == selectedCategory.value;
|
||||
|
||||
return matchesSearch && matchesCategory;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
sortFilteredList();
|
||||
_logger.d('✅ [filterPaket] Filtered to ${filteredPackages.length} packages');
|
||||
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e('❌ [filterPaket] Error filtering packages',
|
||||
error: e,
|
||||
stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the filtered list
|
||||
/// Sort the filtered list based on the selected sort option
|
||||
void sortFilteredList() {
|
||||
switch (sortBy.value) {
|
||||
case 'Terbaru':
|
||||
filteredPaketList.sort(
|
||||
(a, b) => b['created_at'].compareTo(a['created_at']),
|
||||
);
|
||||
break;
|
||||
case 'Terlama':
|
||||
filteredPaketList.sort(
|
||||
(a, b) => a['created_at'].compareTo(b['created_at']),
|
||||
);
|
||||
break;
|
||||
case 'Harga Tertinggi':
|
||||
filteredPaketList.sort((a, b) => b['harga'].compareTo(a['harga']));
|
||||
break;
|
||||
case 'Harga Terendah':
|
||||
filteredPaketList.sort((a, b) => a['harga'].compareTo(b['harga']));
|
||||
break;
|
||||
case 'Nama A-Z':
|
||||
filteredPaketList.sort((a, b) => a['nama'].compareTo(b['nama']));
|
||||
break;
|
||||
case 'Nama Z-A':
|
||||
filteredPaketList.sort((a, b) => b['nama'].compareTo(a['nama']));
|
||||
break;
|
||||
try {
|
||||
_logger.d('🔄 [sortFilteredList] Sorting packages by ${sortBy.value}');
|
||||
|
||||
// Sort new packages
|
||||
switch (sortBy.value) {
|
||||
case 'Terbaru':
|
||||
filteredPackages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
break;
|
||||
case 'Terlama':
|
||||
filteredPackages.sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||
break;
|
||||
case 'Harga Tertinggi':
|
||||
filteredPackages.sort((a, b) => b.harga.compareTo(a.harga));
|
||||
break;
|
||||
case 'Harga Terendah':
|
||||
filteredPackages.sort((a, b) => a.harga.compareTo(b.harga));
|
||||
break;
|
||||
case 'Nama A-Z':
|
||||
filteredPackages.sort((a, b) => a.nama.compareTo(b.nama));
|
||||
break;
|
||||
case 'Nama Z-A':
|
||||
filteredPackages.sort((a, b) => b.nama.compareTo(a.nama));
|
||||
break;
|
||||
}
|
||||
|
||||
// Also sort legacy list for backward compatibility
|
||||
switch (sortBy.value) {
|
||||
case 'Terbaru':
|
||||
filteredPaketList.sort((a, b) =>
|
||||
((b['created_at'] ?? '') as String).compareTo((a['created_at'] ?? '') as String));
|
||||
break;
|
||||
case 'Terlama':
|
||||
filteredPaketList.sort((a, b) =>
|
||||
((a['created_at'] ?? '') as String).compareTo((b['created_at'] ?? '') as String));
|
||||
break;
|
||||
case 'Harga Tertinggi':
|
||||
filteredPaketList.sort((a, b) =>
|
||||
((b['harga'] ?? 0) as int).compareTo((a['harga'] ?? 0) as int));
|
||||
break;
|
||||
case 'Harga Terendah':
|
||||
filteredPaketList.sort((a, b) =>
|
||||
((a['harga'] ?? 0) as int).compareTo((b['harga'] ?? 0) as int));
|
||||
break;
|
||||
case 'Nama A-Z':
|
||||
filteredPaketList.sort((a, b) =>
|
||||
((a['nama'] ?? '') as String).compareTo((b['nama'] ?? '') as String));
|
||||
break;
|
||||
case 'Nama Z-A':
|
||||
filteredPaketList.sort((a, b) =>
|
||||
((b['nama'] ?? '') as String).compareTo((a['nama'] ?? '') as String));
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.d('✅ [sortFilteredList] Sorted ${filteredPackages.length} packages');
|
||||
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e('❌ [sortFilteredList] Error sorting packages',
|
||||
error: e,
|
||||
stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,40 +260,134 @@ class PetugasPaketController extends GetxController {
|
||||
}
|
||||
|
||||
// Tambah paket baru
|
||||
void addPaket(Map<String, dynamic> paket) {
|
||||
paketList.add(paket);
|
||||
filterPaket();
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Paket baru berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
Future<void> addPaket(Map<String, dynamic> paketData) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// Edit paket
|
||||
void editPaket(String id, Map<String, dynamic> updatedPaket) {
|
||||
final index = paketList.indexWhere((element) => element['id'] == id);
|
||||
if (index >= 0) {
|
||||
paketList[index] = updatedPaket;
|
||||
// Convert to PaketModel
|
||||
final newPaket = PaketModel.fromJson({
|
||||
...paketData,
|
||||
'id': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
});
|
||||
|
||||
// Add to the list
|
||||
packages.add(newPaket);
|
||||
_updateLegacyPaketList();
|
||||
filterPaket();
|
||||
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Paket berhasil diperbarui',
|
||||
'Paket baru berhasil ditambahkan',
|
||||
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
|
||||
void deletePaket(String id) {
|
||||
paketList.removeWhere((element) => element['id'] == id);
|
||||
filterPaket();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Paket berhasil dihapus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
Future<void> deletePaket(String id) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// Remove from the main list
|
||||
packages.removeWhere((pkg) => pkg.id == id);
|
||||
_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)}';
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../services/sewa_service.dart';
|
||||
import '../../../data/models/rental_booking_model.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
|
||||
class PetugasSewaController extends GetxController {
|
||||
// Reactive variables
|
||||
@ -7,7 +10,7 @@ class PetugasSewaController extends GetxController {
|
||||
final searchQuery = ''.obs;
|
||||
final orderIdQuery = ''.obs;
|
||||
final selectedStatusFilter = 'Semua'.obs;
|
||||
final filteredSewaList = <Map<String, dynamic>>[].obs;
|
||||
final filteredSewaList = <SewaModel>[].obs;
|
||||
|
||||
// Filter options
|
||||
final List<String> statusFilters = [
|
||||
@ -15,13 +18,19 @@ class PetugasSewaController extends GetxController {
|
||||
'Menunggu Pembayaran',
|
||||
'Periksa Pembayaran',
|
||||
'Diterima',
|
||||
'Aktif',
|
||||
'Dikembalikan',
|
||||
'Selesai',
|
||||
'Dibatalkan',
|
||||
];
|
||||
|
||||
// Mock data for sewa list
|
||||
final RxList<Map<String, dynamic>> sewaList = <Map<String, dynamic>>[].obs;
|
||||
final RxList<SewaModel> sewaList = <SewaModel>[].obs;
|
||||
|
||||
// Payment option state (per sewa)
|
||||
final Map<String, RxBool> isFullPaymentMap = {};
|
||||
final Map<String, TextEditingController> nominalControllerMap = {};
|
||||
final Map<String, RxString> paymentMethodMap = {};
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -41,25 +50,21 @@ class PetugasSewaController extends GetxController {
|
||||
void _updateFilteredList() {
|
||||
filteredSewaList.value =
|
||||
sewaList.where((sewa) {
|
||||
// Apply search filter
|
||||
final matchesSearch = sewa['nama_warga']
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.contains(searchQuery.value.toLowerCase());
|
||||
|
||||
// Apply order ID filter if provided
|
||||
final matchesOrderId =
|
||||
orderIdQuery.value.isEmpty ||
|
||||
sewa['order_id'].toString().toLowerCase().contains(
|
||||
orderIdQuery.value.toLowerCase(),
|
||||
);
|
||||
final query = searchQuery.value.toLowerCase();
|
||||
// Apply search filter: nama warga, id pesanan, atau asetId
|
||||
final matchesSearch =
|
||||
sewa.wargaNama.toLowerCase().contains(query) ||
|
||||
sewa.id.toLowerCase().contains(query) ||
|
||||
(sewa.asetId != null &&
|
||||
sewa.asetId!.toLowerCase().contains(query));
|
||||
|
||||
// Apply status filter if not 'Semua'
|
||||
final matchesStatus =
|
||||
selectedStatusFilter.value == 'Semua' ||
|
||||
sewa['status'] == selectedStatusFilter.value;
|
||||
sewa.status.toUpperCase() ==
|
||||
selectedStatusFilter.value.toUpperCase();
|
||||
|
||||
return matchesSearch && matchesOrderId && matchesStatus;
|
||||
return matchesSearch && matchesStatus;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@ -68,100 +73,8 @@ class PetugasSewaController extends GetxController {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// Simulate API call delay
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
|
||||
// Populate with mock data
|
||||
sewaList.assignAll([
|
||||
{
|
||||
'id': '1',
|
||||
'order_id': 'SWA-001',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-02-05',
|
||||
'tanggal_selesai': '2025-02-10',
|
||||
'total_biaya': 45000,
|
||||
'status': 'Diterima',
|
||||
'photo_url': 'https://example.com/photo1.jpg',
|
||||
},
|
||||
{
|
||||
'id': '2',
|
||||
'order_id': 'SWA-002',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-02-15',
|
||||
'tanggal_selesai': '2025-02-20',
|
||||
'total_biaya': 30000,
|
||||
'status': 'Selesai',
|
||||
'photo_url': 'https://example.com/photo2.jpg',
|
||||
},
|
||||
{
|
||||
'id': '3',
|
||||
'order_id': 'SWA-003',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-02-25',
|
||||
'tanggal_selesai': '2025-03-01',
|
||||
'total_biaya': 35000,
|
||||
'status': 'Menunggu Pembayaran',
|
||||
'photo_url': 'https://example.com/photo3.jpg',
|
||||
},
|
||||
{
|
||||
'id': '4',
|
||||
'order_id': 'SWA-004',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-03-05',
|
||||
'tanggal_selesai': '2025-03-08',
|
||||
'total_biaya': 20000,
|
||||
'status': 'Periksa Pembayaran',
|
||||
'photo_url': 'https://example.com/photo4.jpg',
|
||||
},
|
||||
{
|
||||
'id': '5',
|
||||
'order_id': 'SWA-005',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-03-12',
|
||||
'tanggal_selesai': '2025-03-14',
|
||||
'total_biaya': 15000,
|
||||
'status': 'Dibatalkan',
|
||||
'photo_url': 'https://example.com/photo5.jpg',
|
||||
},
|
||||
{
|
||||
'id': '6',
|
||||
'order_id': 'SWA-006',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-03-18',
|
||||
'tanggal_selesai': '2025-03-20',
|
||||
'total_biaya': 25000,
|
||||
'status': 'Pembayaran Denda',
|
||||
'photo_url': 'https://example.com/photo6.jpg',
|
||||
},
|
||||
{
|
||||
'id': '7',
|
||||
'order_id': 'SWA-007',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-03-25',
|
||||
'tanggal_selesai': '2025-03-28',
|
||||
'total_biaya': 40000,
|
||||
'status': 'Periksa Denda',
|
||||
'photo_url': 'https://example.com/photo7.jpg',
|
||||
},
|
||||
{
|
||||
'id': '8',
|
||||
'order_id': 'SWA-008',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-04-02',
|
||||
'tanggal_selesai': '2025-04-05',
|
||||
'total_biaya': 10000,
|
||||
'status': 'Dikembalikan',
|
||||
'photo_url': 'https://example.com/photo8.jpg',
|
||||
},
|
||||
]);
|
||||
final data = await SewaService().fetchAllSewa();
|
||||
sewaList.assignAll(data);
|
||||
} catch (e) {
|
||||
print('Error loading sewa data: $e');
|
||||
} finally {
|
||||
@ -196,10 +109,11 @@ class PetugasSewaController extends GetxController {
|
||||
sewaList.where((sewa) {
|
||||
bool matchesStatus =
|
||||
selectedStatusFilter.value == 'Semua' ||
|
||||
sewa['status'] == selectedStatusFilter.value;
|
||||
sewa.status.toUpperCase() ==
|
||||
selectedStatusFilter.value.toUpperCase();
|
||||
bool matchesSearch =
|
||||
searchQuery.value.isEmpty ||
|
||||
sewa['nama_warga'].toLowerCase().contains(
|
||||
sewa.wargaNama.toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
);
|
||||
return matchesStatus && matchesSearch;
|
||||
@ -213,102 +127,367 @@ class PetugasSewaController extends GetxController {
|
||||
|
||||
// Get color based on status
|
||||
Color getStatusColor(String status) {
|
||||
switch (status) {
|
||||
case 'Menunggu Pembayaran':
|
||||
return Colors.orange;
|
||||
case 'Periksa Pembayaran':
|
||||
return Colors.amber.shade700;
|
||||
case 'Diterima':
|
||||
return Colors.blue;
|
||||
case 'Pembayaran Denda':
|
||||
return Colors.deepOrange;
|
||||
case 'Periksa Denda':
|
||||
return Colors.red.shade600;
|
||||
case 'Dikembalikan':
|
||||
return Colors.teal;
|
||||
case 'Sedang Disewa':
|
||||
switch (status.toUpperCase()) {
|
||||
case 'MENUNGGU PEMBAYARAN':
|
||||
return Colors.orangeAccent;
|
||||
case 'PERIKSA PEMBAYARAN':
|
||||
return Colors.amber;
|
||||
case 'DITERIMA':
|
||||
return Colors.blueAccent;
|
||||
case 'AKTIF':
|
||||
return Colors.green;
|
||||
case 'Selesai':
|
||||
case 'PEMBAYARAN DENDA':
|
||||
return Colors.deepOrangeAccent;
|
||||
case 'PERIKSA PEMBAYARAN DENDA':
|
||||
return Colors.redAccent;
|
||||
case 'DIKEMBALIKAN':
|
||||
return Colors.teal;
|
||||
case 'SELESAI':
|
||||
return Colors.purple;
|
||||
case 'Dibatalkan':
|
||||
case 'DIBATALKAN':
|
||||
return Colors.red;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
// Get icon based on status
|
||||
IconData getStatusIcon(String status) {
|
||||
switch (status) {
|
||||
case 'MENUNGGU PEMBAYARAN':
|
||||
return Icons.payments_outlined;
|
||||
case 'PERIKSA PEMBAYARAN':
|
||||
return Icons.fact_check_outlined;
|
||||
case 'DITERIMA':
|
||||
return Icons.check_circle_outlined;
|
||||
case 'AKTIF':
|
||||
return Icons.play_circle_outline;
|
||||
case 'PEMBYARAN DENDA':
|
||||
return Icons.money_off_csred_outlined;
|
||||
case 'PERIKSA PEMBAYARAN DENDA':
|
||||
return Icons.assignment_late_outlined;
|
||||
case 'DIKEMBALIKAN':
|
||||
return Icons.assignment_return_outlined;
|
||||
case 'SELESAI':
|
||||
return Icons.task_alt_outlined;
|
||||
case 'DIBATALKAN':
|
||||
return Icons.cancel_outlined;
|
||||
default:
|
||||
return Icons.help_outline_rounded;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sewa approval (from "Periksa Pembayaran" to "Diterima")
|
||||
void approveSewa(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
final currentStatus = sewa['status'];
|
||||
|
||||
if (currentStatus == 'Periksa Pembayaran') {
|
||||
sewa['status'] = 'Diterima';
|
||||
} else if (currentStatus == 'Periksa Denda') {
|
||||
sewa['status'] = 'Selesai';
|
||||
} else if (currentStatus == 'Menunggu Pembayaran') {
|
||||
sewa['status'] = 'Periksa Pembayaran';
|
||||
final sewa = sewaList[index];
|
||||
final currentStatus = sewa.status;
|
||||
String? newStatus;
|
||||
if (currentStatus == 'PERIKSA PEMBAYARAN') {
|
||||
newStatus = 'DITERIMA';
|
||||
} else if (currentStatus == 'PERIKSA PEMBAYARAN DENDA') {
|
||||
newStatus = 'SELESAI';
|
||||
} else if (currentStatus == 'MENUNGGU PEMBAYARAN') {
|
||||
newStatus = 'PERIKSA PEMBAYARAN';
|
||||
}
|
||||
if (newStatus != null) {
|
||||
sewaList[index] = SewaModel(
|
||||
id: sewa.id,
|
||||
userId: sewa.userId,
|
||||
status: newStatus,
|
||||
waktuMulai: sewa.waktuMulai,
|
||||
waktuSelesai: sewa.waktuSelesai,
|
||||
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||
tipePesanan: sewa.tipePesanan,
|
||||
kuantitas: sewa.kuantitas,
|
||||
asetId: sewa.asetId,
|
||||
asetNama: sewa.asetNama,
|
||||
asetFoto: sewa.asetFoto,
|
||||
paketId: sewa.paketId,
|
||||
paketNama: sewa.paketNama,
|
||||
paketFoto: sewa.paketFoto,
|
||||
totalTagihan: sewa.totalTagihan,
|
||||
wargaNama: sewa.wargaNama,
|
||||
wargaNoHp: sewa.wargaNoHp,
|
||||
wargaAvatar: sewa.wargaAvatar,
|
||||
);
|
||||
sewaList.refresh();
|
||||
}
|
||||
|
||||
sewaList[index] = sewa;
|
||||
sewaList.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sewa rejection or cancellation
|
||||
void rejectSewa(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Dibatalkan';
|
||||
sewaList[index] = sewa;
|
||||
final sewa = sewaList[index];
|
||||
sewaList[index] = SewaModel(
|
||||
id: sewa.id,
|
||||
userId: sewa.userId,
|
||||
status: 'Dibatalkan',
|
||||
waktuMulai: sewa.waktuMulai,
|
||||
waktuSelesai: sewa.waktuSelesai,
|
||||
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||
tipePesanan: sewa.tipePesanan,
|
||||
kuantitas: sewa.kuantitas,
|
||||
asetId: sewa.asetId,
|
||||
asetNama: sewa.asetNama,
|
||||
asetFoto: sewa.asetFoto,
|
||||
paketId: sewa.paketId,
|
||||
paketNama: sewa.paketNama,
|
||||
paketFoto: sewa.paketFoto,
|
||||
totalTagihan: sewa.totalTagihan,
|
||||
wargaNama: sewa.wargaNama,
|
||||
wargaNoHp: sewa.wargaNoHp,
|
||||
wargaAvatar: sewa.wargaAvatar,
|
||||
);
|
||||
sewaList.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Request payment for penalty
|
||||
void requestPenaltyPayment(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Pembayaran Denda';
|
||||
sewaList[index] = sewa;
|
||||
final sewa = sewaList[index];
|
||||
sewaList[index] = SewaModel(
|
||||
id: sewa.id,
|
||||
userId: sewa.userId,
|
||||
status: 'Pembayaran Denda',
|
||||
waktuMulai: sewa.waktuMulai,
|
||||
waktuSelesai: sewa.waktuSelesai,
|
||||
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||
tipePesanan: sewa.tipePesanan,
|
||||
kuantitas: sewa.kuantitas,
|
||||
asetId: sewa.asetId,
|
||||
asetNama: sewa.asetNama,
|
||||
asetFoto: sewa.asetFoto,
|
||||
paketId: sewa.paketId,
|
||||
paketNama: sewa.paketNama,
|
||||
paketFoto: sewa.paketFoto,
|
||||
totalTagihan: sewa.totalTagihan,
|
||||
wargaNama: sewa.wargaNama,
|
||||
wargaNoHp: sewa.wargaNoHp,
|
||||
wargaAvatar: sewa.wargaAvatar,
|
||||
);
|
||||
sewaList.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Mark penalty payment as requiring inspection
|
||||
void markPenaltyForInspection(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Periksa Denda';
|
||||
sewaList[index] = sewa;
|
||||
final sewa = sewaList[index];
|
||||
sewaList[index] = SewaModel(
|
||||
id: sewa.id,
|
||||
userId: sewa.userId,
|
||||
status: 'Periksa Denda',
|
||||
waktuMulai: sewa.waktuMulai,
|
||||
waktuSelesai: sewa.waktuSelesai,
|
||||
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||
tipePesanan: sewa.tipePesanan,
|
||||
kuantitas: sewa.kuantitas,
|
||||
asetId: sewa.asetId,
|
||||
asetNama: sewa.asetNama,
|
||||
asetFoto: sewa.asetFoto,
|
||||
paketId: sewa.paketId,
|
||||
paketNama: sewa.paketNama,
|
||||
paketFoto: sewa.paketFoto,
|
||||
totalTagihan: sewa.totalTagihan,
|
||||
wargaNama: sewa.wargaNama,
|
||||
wargaNoHp: sewa.wargaNoHp,
|
||||
wargaAvatar: sewa.wargaAvatar,
|
||||
);
|
||||
sewaList.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sewa completion
|
||||
void completeSewa(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
void completeSewa(String id) async {
|
||||
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Selesai';
|
||||
sewaList[index] = sewa;
|
||||
final sewa = sewaList[index];
|
||||
sewaList[index] = SewaModel(
|
||||
id: sewa.id,
|
||||
userId: sewa.userId,
|
||||
status: 'Selesai',
|
||||
waktuMulai: sewa.waktuMulai,
|
||||
waktuSelesai: sewa.waktuSelesai,
|
||||
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||
tipePesanan: sewa.tipePesanan,
|
||||
kuantitas: sewa.kuantitas,
|
||||
asetId: sewa.asetId,
|
||||
asetNama: sewa.asetNama,
|
||||
asetFoto: sewa.asetFoto,
|
||||
paketId: sewa.paketId,
|
||||
paketNama: sewa.paketNama,
|
||||
paketFoto: sewa.paketFoto,
|
||||
totalTagihan: sewa.totalTagihan,
|
||||
wargaNama: sewa.wargaNama,
|
||||
wargaNoHp: sewa.wargaNoHp,
|
||||
wargaAvatar: sewa.wargaAvatar,
|
||||
);
|
||||
sewaList.refresh();
|
||||
// Update status in database
|
||||
final asetProvider = Get.find<AsetProvider>();
|
||||
await asetProvider.updateSewaAsetStatus(
|
||||
sewaAsetId: id,
|
||||
status: 'SELESAI',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark rental as returned
|
||||
void markAsReturned(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
Future<void> markAsReturned(String id) async {
|
||||
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Dikembalikan';
|
||||
sewaList[index] = sewa;
|
||||
final sewa = sewaList[index];
|
||||
sewaList[index] = SewaModel(
|
||||
id: sewa.id,
|
||||
userId: sewa.userId,
|
||||
status: 'Dikembalikan',
|
||||
waktuMulai: sewa.waktuMulai,
|
||||
waktuSelesai: sewa.waktuSelesai,
|
||||
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||
tipePesanan: sewa.tipePesanan,
|
||||
kuantitas: sewa.kuantitas,
|
||||
asetId: sewa.asetId,
|
||||
asetNama: sewa.asetNama,
|
||||
asetFoto: sewa.asetFoto,
|
||||
paketId: sewa.paketId,
|
||||
paketNama: sewa.paketNama,
|
||||
paketFoto: sewa.paketFoto,
|
||||
totalTagihan: sewa.totalTagihan,
|
||||
wargaNama: sewa.wargaNama,
|
||||
wargaNoHp: sewa.wargaNoHp,
|
||||
wargaAvatar: sewa.wargaAvatar,
|
||||
);
|
||||
sewaList.refresh();
|
||||
// Update status in database
|
||||
final asetProvider = Get.find<AsetProvider>();
|
||||
final result = await asetProvider.updateSewaAsetStatus(
|
||||
sewaAsetId: id,
|
||||
status: 'DIKEMBALIKAN',
|
||||
);
|
||||
if (!result) {
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Gagal mengubah status sewa di database',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil detail item paket (nama aset & kuantitas)
|
||||
Future<List<Map<String, dynamic>>> getPaketItems(String paketId) async {
|
||||
final asetProvider = Get.find<AsetProvider>();
|
||||
debugPrint('[DEBUG] getPaketItems called with paketId: $paketId');
|
||||
try {
|
||||
final items = await asetProvider.getPaketItems(paketId);
|
||||
debugPrint('[DEBUG] getPaketItems result for paketId $paketId:');
|
||||
for (var item in items) {
|
||||
debugPrint(' - item: ${item.toString()}');
|
||||
}
|
||||
return items;
|
||||
} catch (e, stack) {
|
||||
debugPrint('[ERROR] getPaketItems failed for paketId $paketId: $e');
|
||||
debugPrint('[ERROR] Stacktrace: $stack');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
RxBool getIsFullPayment(String sewaId) {
|
||||
if (!isFullPaymentMap.containsKey(sewaId)) {
|
||||
isFullPaymentMap[sewaId] = false.obs;
|
||||
}
|
||||
return isFullPaymentMap[sewaId]!;
|
||||
}
|
||||
|
||||
TextEditingController getNominalController(String sewaId) {
|
||||
if (!nominalControllerMap.containsKey(sewaId)) {
|
||||
final controller = TextEditingController(text: '0');
|
||||
nominalControllerMap[sewaId] = controller;
|
||||
}
|
||||
return nominalControllerMap[sewaId]!;
|
||||
}
|
||||
|
||||
void setFullPayment(String sewaId, bool value, num totalTagihan) {
|
||||
getIsFullPayment(sewaId).value = value;
|
||||
if (value) {
|
||||
getNominalController(sewaId).text = totalTagihan.toString();
|
||||
}
|
||||
}
|
||||
|
||||
RxString getPaymentMethod(String sewaId) {
|
||||
if (!paymentMethodMap.containsKey(sewaId)) {
|
||||
paymentMethodMap[sewaId] = 'Tunai'.obs;
|
||||
}
|
||||
return paymentMethodMap[sewaId]!;
|
||||
}
|
||||
|
||||
void setPaymentMethod(String sewaId, String method) {
|
||||
getPaymentMethod(sewaId).value = method;
|
||||
}
|
||||
|
||||
Future<String?> getTagihanSewaIdBySewaAsetId(String sewaAsetId) async {
|
||||
final asetProvider = Get.find<AsetProvider>();
|
||||
final tagihan = await asetProvider.getTagihanSewa(sewaAsetId);
|
||||
if (tagihan != null && tagihan['id'] != null) {
|
||||
return tagihan['id'] as String;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> confirmPembayaranTagihan({
|
||||
required String sewaAsetId,
|
||||
required int nominal,
|
||||
required String metodePembayaran,
|
||||
}) async {
|
||||
final tagihanSewaId = await getTagihanSewaIdBySewaAsetId(sewaAsetId);
|
||||
if (tagihanSewaId == null) {
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Tagihan sewa tidak ditemukan',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final asetProvider = Get.find<AsetProvider>();
|
||||
// Cek status sewa_aset saat ini
|
||||
final sewaAsetData = await asetProvider.getSewaAsetWithAsetData(sewaAsetId);
|
||||
if (sewaAsetData != null &&
|
||||
(sewaAsetData['status']?.toString()?.toUpperCase() ==
|
||||
'PERIKSA PEMBAYARAN')) {
|
||||
// Ubah status menjadi MENUNGGU PEMBAYARAN
|
||||
await asetProvider.updateSewaAsetStatus(
|
||||
sewaAsetId: sewaAsetId,
|
||||
status: 'MENUNGGU PEMBAYARAN',
|
||||
);
|
||||
}
|
||||
final result = await asetProvider.processPembayaranTagihan(
|
||||
tagihanSewaId: tagihanSewaId,
|
||||
nominal: nominal,
|
||||
metodePembayaran: metodePembayaran,
|
||||
);
|
||||
if (result) {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Pembayaran berhasil diproses',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Pembayaran gagal diproses',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,187 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:bumrent_app/app/data/models/aset_model.dart';
|
||||
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||
|
||||
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
|
||||
final nameController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
@ -23,27 +203,17 @@ class PetugasTambahAsetController extends GetxController {
|
||||
final categoryOptions = ['Sewa', 'Langganan'];
|
||||
final statusOptions = ['Tersedia', 'Pemeliharaan'];
|
||||
|
||||
// Images
|
||||
final selectedImages = <String>[].obs;
|
||||
// List to store selected images
|
||||
final RxList<XFile> selectedImages = <XFile>[].obs;
|
||||
// List to store network image URLs
|
||||
final RxList<String> networkImageUrls = <String>[].obs;
|
||||
final _picker = ImagePicker();
|
||||
|
||||
// Form validation
|
||||
final isFormValid = false.obs;
|
||||
final isSubmitting = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// Set default values
|
||||
quantityController.text = '1';
|
||||
unitOfMeasureController.text = 'Unit';
|
||||
|
||||
// Listen to field changes for validation
|
||||
nameController.addListener(validateForm);
|
||||
descriptionController.addListener(validateForm);
|
||||
quantityController.addListener(validateForm);
|
||||
pricePerHourController.addListener(validateForm);
|
||||
pricePerDayController.addListener(validateForm);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
@ -89,17 +259,140 @@ class PetugasTambahAsetController extends GetxController {
|
||||
validateForm();
|
||||
}
|
||||
|
||||
// Add image to the list (in a real app, this would handle file upload)
|
||||
void addImage(String imagePath) {
|
||||
selectedImages.add(imagePath);
|
||||
validateForm();
|
||||
|
||||
// Create a new asset in Supabase
|
||||
Future<String?> _createAsset(
|
||||
Map<String, dynamic> assetData,
|
||||
List<Map<String, dynamic>> satuanWaktuSewa,
|
||||
) async {
|
||||
try {
|
||||
// Create the asset in the 'aset' table
|
||||
final response = await _asetProvider.createAset(assetData);
|
||||
|
||||
if (response == null || response['id'] == null) {
|
||||
debugPrint('❌ Failed to create asset: No response or ID from server');
|
||||
return null;
|
||||
}
|
||||
|
||||
final String assetId = response['id'].toString();
|
||||
debugPrint('✅ Asset created with ID: $assetId');
|
||||
|
||||
// Add satuan waktu sewa
|
||||
for (var sws in satuanWaktuSewa) {
|
||||
final success = await _asetProvider.addSatuanWaktuSewa(
|
||||
asetId: assetId,
|
||||
satuanWaktu: sws['satuan_waktu'],
|
||||
harga: sws['harga'],
|
||||
maksimalWaktu: sws['maksimal_waktu'],
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
debugPrint('❌ Failed to add satuan waktu sewa: $sws');
|
||||
}
|
||||
}
|
||||
|
||||
return assetId;
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ Error creating asset: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove image from the list
|
||||
// Update an existing asset in Supabase
|
||||
Future<bool> _updateAsset(
|
||||
String assetId,
|
||||
Map<String, dynamic> assetData,
|
||||
List<Map<String, dynamic>> satuanWaktuSewa,
|
||||
) async {
|
||||
try {
|
||||
debugPrint('\n🔄 Starting update for asset ID: $assetId');
|
||||
|
||||
// 1. Extract and remove foto_aset from assetData as it's not in the aset table
|
||||
final fotoAsetUrl = assetData['foto_aset'];
|
||||
assetData.remove('foto_aset');
|
||||
debugPrint('📝 Asset data prepared for update (without foto_aset)');
|
||||
|
||||
// 2. Update the main asset data (without foto_aset)
|
||||
debugPrint('🔄 Updating main asset data...');
|
||||
final success = await _asetProvider.updateAset(assetId, assetData);
|
||||
if (!success) {
|
||||
debugPrint('❌ Failed to update asset with ID: $assetId');
|
||||
return false;
|
||||
}
|
||||
debugPrint('✅ Successfully updated main asset data');
|
||||
|
||||
// 3. Update satuan waktu sewa
|
||||
debugPrint('\n🔄 Updating rental time units...');
|
||||
// First, delete existing satuan waktu sewa
|
||||
await _asetProvider.deleteSatuanWaktuSewaByAsetId(assetId);
|
||||
|
||||
// Then add the new ones
|
||||
for (var sws in satuanWaktuSewa) {
|
||||
debugPrint(' - Adding: ${sws['satuan_waktu']} (${sws['harga']} IDR)');
|
||||
await _asetProvider.addSatuanWaktuSewa(
|
||||
asetId: assetId,
|
||||
satuanWaktu: sws['satuan_waktu'],
|
||||
harga: sws['harga'] as int,
|
||||
maksimalWaktu: sws['maksimal_waktu'] as int,
|
||||
);
|
||||
}
|
||||
debugPrint('✅ Successfully updated rental time units');
|
||||
|
||||
// 4. Update photos in the foto_aset table if any exist
|
||||
if (selectedImages.isNotEmpty || networkImageUrls.isNotEmpty) {
|
||||
// Combine network URLs and local file paths
|
||||
final List<String> allImageUrls = [
|
||||
...networkImageUrls,
|
||||
...selectedImages.map((file) => file.path),
|
||||
];
|
||||
|
||||
debugPrint('\n🖼️ Processing photos for asset $assetId');
|
||||
debugPrint(' - Network URLs: ${networkImageUrls.length}');
|
||||
debugPrint(' - Local files: ${selectedImages.length}');
|
||||
debugPrint(' - Total unique photos: ${allImageUrls.toSet().length} (before deduplication)');
|
||||
|
||||
try {
|
||||
// Use updateFotoAset which handles both uploading new photos and updating the database
|
||||
final photoSuccess = await _asetProvider.updateFotoAset(
|
||||
asetId: assetId,
|
||||
fotoUrls: allImageUrls,
|
||||
);
|
||||
|
||||
if (!photoSuccess) {
|
||||
debugPrint('⚠️ Some photos might not have been updated for asset $assetId');
|
||||
// We don't fail the whole update if photo update fails
|
||||
// as the main asset data has been saved successfully
|
||||
} else {
|
||||
debugPrint('✅ Successfully updated photos for asset $assetId');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ Error updating photos: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
// Continue with the update even if photo update fails
|
||||
}
|
||||
} else {
|
||||
debugPrint('ℹ️ No photos to update');
|
||||
}
|
||||
|
||||
debugPrint('\n✅ Asset update completed successfully for ID: $assetId');
|
||||
return true;
|
||||
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ Error updating asset: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove an image from the selected images list
|
||||
void removeImage(int index) {
|
||||
if (index >= 0 && index < selectedImages.length) {
|
||||
// Remove from both lists if they have an entry at this index
|
||||
if (index < networkImageUrls.length) {
|
||||
networkImageUrls.removeAt(index);
|
||||
}
|
||||
selectedImages.removeAt(index);
|
||||
validateForm();
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,62 +426,130 @@ class PetugasTambahAsetController extends GetxController {
|
||||
basicValid && perHourValid && perDayValid && anyTimeOptionSelected;
|
||||
}
|
||||
|
||||
// Submit form and save asset
|
||||
// Submit form and save or update asset
|
||||
Future<void> saveAsset() async {
|
||||
if (!isFormValid.value) return;
|
||||
|
||||
isSubmitting.value = true;
|
||||
|
||||
try {
|
||||
// In a real app, this would make an API call to save the asset
|
||||
await Future.delayed(const Duration(seconds: 1)); // Mock API call
|
||||
|
||||
// Prepare asset data
|
||||
final assetData = {
|
||||
// Prepare the basic asset data
|
||||
final Map<String, dynamic> assetData = {
|
||||
'nama': nameController.text,
|
||||
'deskripsi': descriptionController.text,
|
||||
'kategori': selectedCategory.value,
|
||||
'kategori': 'sewa', // Default to 'sewa' category
|
||||
'status': selectedStatus.value,
|
||||
'kuantitas': int.parse(quantityController.text),
|
||||
'satuan_ukur': unitOfMeasureController.text,
|
||||
'opsi_waktu_sewa':
|
||||
timeOptions.entries
|
||||
.where((entry) => entry.value.value)
|
||||
.map((entry) => entry.key)
|
||||
.toList(),
|
||||
'harga_per_jam':
|
||||
timeOptions['Per Jam']!.value
|
||||
? int.parse(pricePerHourController.text)
|
||||
: null,
|
||||
'max_jam':
|
||||
timeOptions['Per Jam']!.value && maxHourController.text.isNotEmpty
|
||||
? int.parse(maxHourController.text)
|
||||
: null,
|
||||
'harga_per_hari':
|
||||
timeOptions['Per Hari']!.value
|
||||
? int.parse(pricePerDayController.text)
|
||||
: null,
|
||||
'max_hari':
|
||||
timeOptions['Per Hari']!.value && maxDayController.text.isNotEmpty
|
||||
? int.parse(maxDayController.text)
|
||||
: null,
|
||||
'gambar': selectedImages,
|
||||
'satuan_ukur': 'unit', // Default unit of measure
|
||||
};
|
||||
|
||||
// Log the data (in a real app, this would be sent to an API)
|
||||
print('Asset data: $assetData');
|
||||
// Handle time options and pricing
|
||||
final List<Map<String, dynamic>> satuanWaktuSewa = [];
|
||||
|
||||
// Return to the asset list page
|
||||
Get.back();
|
||||
if (timeOptions['Per Jam']?.value == true) {
|
||||
final hargaPerJam = int.tryParse(pricePerHourController.text) ?? 0;
|
||||
final maxJam = int.tryParse(maxHourController.text) ?? 24;
|
||||
|
||||
// Show success message
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Aset berhasil ditambahkan',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
if (hargaPerJam <= 0) {
|
||||
throw Exception('Harga per jam harus lebih dari 0');
|
||||
}
|
||||
|
||||
satuanWaktuSewa.add({
|
||||
'satuan_waktu': 'jam',
|
||||
'harga': hargaPerJam,
|
||||
'maksimal_waktu': maxJam,
|
||||
});
|
||||
}
|
||||
|
||||
if (timeOptions['Per Hari']?.value == true) {
|
||||
final hargaPerHari = int.tryParse(pricePerDayController.text) ?? 0;
|
||||
final maxHari = int.tryParse(maxDayController.text) ?? 30;
|
||||
|
||||
if (hargaPerHari <= 0) {
|
||||
throw Exception('Harga per hari harus lebih dari 0');
|
||||
}
|
||||
|
||||
satuanWaktuSewa.add({
|
||||
'satuan_waktu': 'hari',
|
||||
'harga': hargaPerHari,
|
||||
'maksimal_waktu': maxHari,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate that at least one time option is selected
|
||||
if (satuanWaktuSewa.isEmpty) {
|
||||
throw Exception('Pilih setidaknya satu opsi waktu sewa (jam/hari)');
|
||||
}
|
||||
|
||||
// Handle image uploads
|
||||
List<String> imageUrls = [];
|
||||
|
||||
if (networkImageUrls.isNotEmpty) {
|
||||
// Use existing network URLs
|
||||
imageUrls = List.from(networkImageUrls);
|
||||
} else if (selectedImages.isNotEmpty) {
|
||||
// For local files, we'll upload them to Supabase Storage
|
||||
// Store the file paths for now, they'll be uploaded in the provider
|
||||
imageUrls = selectedImages.map((file) => file.path).toList();
|
||||
debugPrint('Found ${imageUrls.length} local images to upload');
|
||||
} else if (!isEditing.value) {
|
||||
// For new assets, require at least one image
|
||||
throw Exception('Harap unggah setidaknya satu gambar');
|
||||
}
|
||||
|
||||
// Ensure at least one image is provided for new assets
|
||||
if (imageUrls.isEmpty && !isEditing.value) {
|
||||
throw Exception('Harap unggah setidaknya satu gambar');
|
||||
}
|
||||
|
||||
// Create or update the asset
|
||||
bool success;
|
||||
String? createdAssetId;
|
||||
|
||||
if (isEditing.value && (assetId?.isNotEmpty ?? false)) {
|
||||
// Update existing asset
|
||||
debugPrint('🔄 Updating asset with ID: $assetId');
|
||||
success = await _updateAsset(assetId!, assetData, satuanWaktuSewa);
|
||||
|
||||
// Update all photos if we have any
|
||||
if (success && imageUrls.isNotEmpty) {
|
||||
await _asetProvider.updateFotoAset(
|
||||
asetId: assetId!,
|
||||
fotoUrls: imageUrls,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Create new asset
|
||||
debugPrint('🔄 Creating new asset');
|
||||
createdAssetId = await _createAsset(assetData, satuanWaktuSewa);
|
||||
success = createdAssetId != null;
|
||||
|
||||
// Add all photos for new asset
|
||||
if (success && createdAssetId != null && imageUrls.isNotEmpty) {
|
||||
await _asetProvider.updateFotoAset(
|
||||
asetId: createdAssetId,
|
||||
fotoUrls: imageUrls,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// Show success message
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
isEditing.value ? 'Aset berhasil diperbarui' : 'Aset berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.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) {
|
||||
// Show error message
|
||||
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
|
||||
void addSampleImage() {
|
||||
addImage('assets/images/sample_asset_${selectedImages.length + 1}.jpg');
|
||||
// In a real app, this would open the image picker
|
||||
selectedImages.add(XFile('assets/images/sample_asset_${selectedImages.length + 1}.jpg'));
|
||||
validateForm();
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:bumrent_app/app/data/models/paket_model.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||
import 'dart:io';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class PetugasTambahPaketController extends GetxController {
|
||||
// Form controllers
|
||||
@ -10,14 +16,14 @@ class PetugasTambahPaketController extends GetxController {
|
||||
|
||||
// Dropdown and toggle values
|
||||
final selectedCategory = 'Bulanan'.obs;
|
||||
final selectedStatus = 'Aktif'.obs;
|
||||
final selectedStatus = 'Tersedia'.obs;
|
||||
|
||||
// Category options
|
||||
final categoryOptions = ['Bulanan', 'Tahunan', 'Premium', 'Bisnis'];
|
||||
final statusOptions = ['Aktif', 'Nonaktif'];
|
||||
final statusOptions = ['Tersedia', 'Pemeliharaan'];
|
||||
|
||||
// Images
|
||||
final selectedImages = <String>[].obs;
|
||||
final selectedImages = <dynamic>[].obs;
|
||||
|
||||
// For package name and description
|
||||
final packageNameController = TextEditingController();
|
||||
@ -31,21 +37,85 @@ class PetugasTambahPaketController extends GetxController {
|
||||
// For asset selection
|
||||
final RxList<Map<String, dynamic>> availableAssets =
|
||||
<Map<String, dynamic>>[].obs;
|
||||
final Rx<int?> selectedAsset = Rx<int?>(null);
|
||||
final Rx<String?> selectedAsset = Rx<String?>(null);
|
||||
final RxBool isLoadingAssets = false.obs;
|
||||
|
||||
// Form validation
|
||||
final isFormValid = false.obs;
|
||||
final isSubmitting = false.obs;
|
||||
|
||||
// New RxBool for editing
|
||||
final isEditing = false.obs;
|
||||
|
||||
final timeOptions = {'Per Jam': true.obs, 'Per Hari': false.obs};
|
||||
final pricePerHourController = TextEditingController();
|
||||
final maxHourController = TextEditingController();
|
||||
final pricePerDayController = TextEditingController();
|
||||
final maxDayController = TextEditingController();
|
||||
|
||||
final _picker = ImagePicker();
|
||||
|
||||
final isFormChanged = false.obs;
|
||||
Map<String, dynamic> initialFormData = {};
|
||||
|
||||
final AsetProvider _asetProvider = Get.put(AsetProvider());
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// Ambil flag isEditing 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
|
||||
nameController.addListener(validateForm);
|
||||
descriptionController.addListener(validateForm);
|
||||
priceController.addListener(validateForm);
|
||||
nameController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
descriptionController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
priceController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
itemQuantityController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
pricePerHourController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
maxHourController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
pricePerDayController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
maxDayController.addListener(() {
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
});
|
||||
|
||||
// Load available assets when the controller initializes
|
||||
fetchAvailableAssets();
|
||||
@ -61,6 +131,10 @@ class PetugasTambahPaketController extends GetxController {
|
||||
packageNameController.dispose();
|
||||
packageDescriptionController.dispose();
|
||||
packagePriceController.dispose();
|
||||
pricePerHourController.dispose();
|
||||
maxHourController.dispose();
|
||||
pricePerDayController.dispose();
|
||||
maxDayController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@ -68,18 +142,21 @@ class PetugasTambahPaketController extends GetxController {
|
||||
void setCategory(String category) {
|
||||
selectedCategory.value = category;
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
}
|
||||
|
||||
// Change selected status
|
||||
void setStatus(String status) {
|
||||
selectedStatus.value = status;
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
}
|
||||
|
||||
// Add image to the list (in a real app, this would handle file upload)
|
||||
void addImage(String imagePath) {
|
||||
selectedImages.add(imagePath);
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
}
|
||||
|
||||
// Remove image from the list
|
||||
@ -87,34 +164,43 @@ class PetugasTambahPaketController extends GetxController {
|
||||
if (index >= 0 && index < selectedImages.length) {
|
||||
selectedImages.removeAt(index);
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch available assets from the API or local data
|
||||
void fetchAvailableAssets() {
|
||||
// Fetch available assets from Supabase and filter out already selected ones
|
||||
Future<void> fetchAvailableAssets() async {
|
||||
isLoadingAssets.value = true;
|
||||
|
||||
// This is a mock implementation - replace with actual API call
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
availableAssets.value = [
|
||||
{'id': 1, 'nama': 'Laptop Dell XPS', 'stok': 5},
|
||||
{'id': 2, 'nama': 'Proyektor Epson', 'stok': 3},
|
||||
{'id': 3, 'nama': 'Meja Kantor', 'stok': 10},
|
||||
{'id': 4, 'nama': 'Kursi Ergonomis', 'stok': 15},
|
||||
{'id': 5, 'nama': 'Printer HP LaserJet', 'stok': 2},
|
||||
{'id': 6, 'nama': 'AC Panasonic 1PK', 'stok': 8},
|
||||
];
|
||||
try {
|
||||
final allAssets = await _asetProvider.getSewaAsets();
|
||||
final selectedAsetIds =
|
||||
packageItems.map((item) => item['asetId'].toString()).toSet();
|
||||
// Only show assets not yet selected
|
||||
availableAssets.value =
|
||||
allAssets
|
||||
.where((aset) => !selectedAsetIds.contains(aset.id))
|
||||
.map(
|
||||
(aset) => {
|
||||
'id': aset.id,
|
||||
'nama': aset.nama,
|
||||
'stok': aset.kuantitas,
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
availableAssets.value = [];
|
||||
} finally {
|
||||
isLoadingAssets.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Set the selected asset
|
||||
void setSelectedAsset(int? assetId) {
|
||||
void setSelectedAsset(String? assetId) {
|
||||
selectedAsset.value = assetId;
|
||||
}
|
||||
|
||||
// Get remaining stock for an asset (considering current selections)
|
||||
int getRemainingStock(int assetId) {
|
||||
int getRemainingStock(String assetId) {
|
||||
// Find the asset in available assets
|
||||
final asset = availableAssets.firstWhere(
|
||||
(item) => item['id'] == assetId,
|
||||
@ -129,7 +215,7 @@ class PetugasTambahPaketController extends GetxController {
|
||||
// Calculate how many of this asset are already in the package
|
||||
int alreadySelected = 0;
|
||||
for (var item in packageItems) {
|
||||
if (item['asetId'] == assetId) {
|
||||
if (item['asetId'].toString() == assetId) {
|
||||
alreadySelected += item['jumlah'] as int;
|
||||
}
|
||||
}
|
||||
@ -204,6 +290,8 @@ class PetugasTambahPaketController extends GetxController {
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
checkFormChanged();
|
||||
}
|
||||
|
||||
// Update an existing package item
|
||||
@ -301,11 +389,16 @@ class PetugasTambahPaketController extends GetxController {
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
checkFormChanged();
|
||||
}
|
||||
|
||||
// Remove an item from the package
|
||||
void removeItem(int index) {
|
||||
packageItems.removeAt(index);
|
||||
if (index >= 0 && index < packageItems.length) {
|
||||
packageItems.removeAt(index);
|
||||
checkFormChanged();
|
||||
}
|
||||
Get.snackbar(
|
||||
'Dihapus',
|
||||
'Item berhasil dihapus dari paket',
|
||||
@ -319,10 +412,7 @@ class PetugasTambahPaketController extends GetxController {
|
||||
void validateForm() {
|
||||
// Basic validation
|
||||
bool basicValid =
|
||||
nameController.text.isNotEmpty &&
|
||||
descriptionController.text.isNotEmpty &&
|
||||
priceController.text.isNotEmpty &&
|
||||
int.tryParse(priceController.text) != null;
|
||||
nameController.text.isNotEmpty && descriptionController.text.isNotEmpty;
|
||||
|
||||
// Package should have at least one item
|
||||
bool hasItems = packageItems.isNotEmpty;
|
||||
@ -337,39 +427,204 @@ class PetugasTambahPaketController extends GetxController {
|
||||
isSubmitting.value = true;
|
||||
|
||||
try {
|
||||
// In a real app, this would make an API call to save the package
|
||||
await Future.delayed(const Duration(seconds: 1)); // Mock API call
|
||||
final supabase = Supabase.instance.client;
|
||||
if (isEditing.value) {
|
||||
// --- UPDATE LOGIC ---
|
||||
final paketArg = Get.arguments['paket'];
|
||||
final String paketId =
|
||||
paketArg is Map && paketArg['id'] != null
|
||||
? paketArg['id'].toString()
|
||||
: (paketArg is PaketModel && paketArg.id != null
|
||||
? paketArg.id.toString()
|
||||
: '');
|
||||
if (paketId.isEmpty) throw Exception('ID paket tidak ditemukan');
|
||||
|
||||
// Prepare package data
|
||||
final paketData = {
|
||||
'nama': nameController.text,
|
||||
'deskripsi': descriptionController.text,
|
||||
'kategori': selectedCategory.value,
|
||||
'status': selectedStatus.value == 'Aktif',
|
||||
'harga': int.parse(priceController.text),
|
||||
'gambar': selectedImages,
|
||||
'items': packageItems,
|
||||
};
|
||||
// 1. Update data utama paket
|
||||
await supabase
|
||||
.from('paket')
|
||||
.update({
|
||||
'nama': nameController.text,
|
||||
'deskripsi': descriptionController.text,
|
||||
'status': selectedStatus.value.toLowerCase(),
|
||||
})
|
||||
.eq('id', paketId);
|
||||
|
||||
// Log the data (in a real app, this would be sent to an API)
|
||||
print('Package data: $paketData');
|
||||
// 2. Update paket_item: hapus semua, insert ulang
|
||||
await supabase.from('paket_item').delete().eq('paket_id', paketId);
|
||||
for (var item in packageItems) {
|
||||
await supabase.from('paket_item').insert({
|
||||
'paket_id': paketId,
|
||||
'aset_id': item['asetId'],
|
||||
'kuantitas': item['jumlah'],
|
||||
});
|
||||
}
|
||||
|
||||
// Return to the package list page
|
||||
Get.back();
|
||||
// 3. Update satuan_waktu_sewa: hapus semua, insert ulang
|
||||
await supabase
|
||||
.from('satuan_waktu_sewa')
|
||||
.delete()
|
||||
.eq('paket_id', paketId);
|
||||
// Fetch satuan_waktu UUIDs
|
||||
final satuanWaktuList = await supabase
|
||||
.from('satuan_waktu')
|
||||
.select('id, nama_satuan_waktu');
|
||||
String? jamId;
|
||||
String? hariId;
|
||||
for (var sw in satuanWaktuList) {
|
||||
final nama = (sw['nama_satuan_waktu'] ?? '').toString().toLowerCase();
|
||||
if (nama.contains('jam')) jamId = sw['id'];
|
||||
if (nama.contains('hari')) hariId = sw['id'];
|
||||
}
|
||||
if (timeOptions['Per Jam']?.value == true && jamId != null) {
|
||||
await supabase.from('satuan_waktu_sewa').insert({
|
||||
'paket_id': paketId,
|
||||
'satuan_waktu_id': jamId,
|
||||
'harga': int.tryParse(pricePerHourController.text) ?? 0,
|
||||
'maksimal_waktu': int.tryParse(maxHourController.text) ?? 0,
|
||||
});
|
||||
}
|
||||
if (timeOptions['Per Hari']?.value == true && hariId != null) {
|
||||
await supabase.from('satuan_waktu_sewa').insert({
|
||||
'paket_id': paketId,
|
||||
'satuan_waktu_id': hariId,
|
||||
'harga': int.tryParse(pricePerDayController.text) ?? 0,
|
||||
'maksimal_waktu': int.tryParse(maxDayController.text) ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Show success message
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Paket berhasil ditambahkan',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
// 4. Update foto_aset
|
||||
// a. Ambil foto lama dari DB
|
||||
final oldPhotos = await supabase
|
||||
.from('foto_aset')
|
||||
.select('foto_aset')
|
||||
.eq('id_paket', paketId);
|
||||
final oldPhotoUrls =
|
||||
oldPhotos
|
||||
.map((e) => e['foto_aset']?.toString())
|
||||
.whereType<String>()
|
||||
.toSet();
|
||||
final newPhotoUrls =
|
||||
selectedImages
|
||||
.map((img) => img is String ? img : (img.path ?? ''))
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toSet();
|
||||
// b. Hapus foto yang dihapus user (dari DB dan storage)
|
||||
final removedPhotos = oldPhotoUrls.difference(newPhotoUrls);
|
||||
for (final url in removedPhotos) {
|
||||
await supabase
|
||||
.from('foto_aset')
|
||||
.delete()
|
||||
.eq('foto_aset', url)
|
||||
.eq('id_paket', paketId);
|
||||
await _asetProvider.deleteFileFromStorage(url);
|
||||
}
|
||||
// c. Tambah foto baru (upload jika perlu, insert ke DB)
|
||||
for (final img in selectedImages) {
|
||||
String url = '';
|
||||
if (img is String && img.startsWith('http')) {
|
||||
url = img;
|
||||
} else if (img is XFile) {
|
||||
final uploaded = await _asetProvider.uploadFileToStorage(
|
||||
File(img.path),
|
||||
);
|
||||
if (uploaded != null) url = uploaded;
|
||||
}
|
||||
if (url.isNotEmpty && !oldPhotoUrls.contains(url)) {
|
||||
await supabase.from('foto_aset').insert({
|
||||
'id_paket': paketId,
|
||||
'foto_aset': url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sukses
|
||||
Get.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) {
|
||||
// Show error message
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan: ${e.toString()}',
|
||||
'Terjadi kesalahan: \\${e.toString()}',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
@ -390,4 +645,215 @@ class PetugasTambahPaketController extends GetxController {
|
||||
selectedImages.add('https://example.com/sample_image.jpg');
|
||||
validateForm();
|
||||
}
|
||||
|
||||
void toggleTimeOption(String option) {
|
||||
timeOptions[option]?.value = !(timeOptions[option]?.value ?? false);
|
||||
// Ensure at least one option is selected
|
||||
bool anySelected = false;
|
||||
timeOptions.forEach((key, value) {
|
||||
if (value.value) anySelected = true;
|
||||
});
|
||||
if (!anySelected) {
|
||||
timeOptions[option]?.value = true;
|
||||
}
|
||||
validateForm();
|
||||
checkFormChanged();
|
||||
}
|
||||
|
||||
Future<void> fetchPaketDetail(String paketId) async {
|
||||
try {
|
||||
debugPrint('[DEBUG] Fetching paket detail for id: $paketId');
|
||||
final supabase = Supabase.instance.client;
|
||||
// 1) Ambil data paket utama
|
||||
final paketData =
|
||||
await supabase
|
||||
.from('paket')
|
||||
.select('id, nama, deskripsi, status')
|
||||
.eq('id', paketId)
|
||||
.single();
|
||||
debugPrint('[DEBUG] Paket data: ' + paketData.toString());
|
||||
|
||||
// 2) Ambil paket_item
|
||||
final paketItemData = await supabase
|
||||
.from('paket_item')
|
||||
.select('id, paket_id, aset_id, kuantitas')
|
||||
.eq('paket_id', paketId);
|
||||
debugPrint('[DEBUG] Paket item data: ' + paketItemData.toString());
|
||||
|
||||
// 3) Ambil satuan_waktu_sewa
|
||||
final swsData = await supabase
|
||||
.from('satuan_waktu_sewa')
|
||||
.select('id, paket_id, satuan_waktu_id, harga, maksimal_waktu')
|
||||
.eq('paket_id', paketId);
|
||||
debugPrint('[DEBUG] Satuan waktu sewa data: ' + swsData.toString());
|
||||
|
||||
// 4) Ambil semua satuan_waktu_id dari swsData
|
||||
final swIds = swsData.map((e) => e['satuan_waktu_id']).toSet().toList();
|
||||
final swData =
|
||||
swIds.isNotEmpty
|
||||
? await supabase
|
||||
.from('satuan_waktu')
|
||||
.select('id, nama_satuan_waktu')
|
||||
.inFilter('id', swIds)
|
||||
: [];
|
||||
debugPrint('[DEBUG] Satuan waktu data: ' + swData.toString());
|
||||
final Map satuanWaktuMap = {
|
||||
for (var sw in swData) sw['id']: sw['nama_satuan_waktu'],
|
||||
};
|
||||
|
||||
// 5) Ambil foto_aset
|
||||
final fotoData = await supabase
|
||||
.from('foto_aset')
|
||||
.select('id_paket, foto_aset')
|
||||
.eq('id_paket', paketId);
|
||||
debugPrint('[DEBUG] Foto aset data: ' + fotoData.toString());
|
||||
|
||||
// 6) Kumpulkan semua aset_id dari paketItemData
|
||||
final asetIds = paketItemData.map((e) => e['aset_id']).toSet().toList();
|
||||
final asetData =
|
||||
asetIds.isNotEmpty
|
||||
? await supabase
|
||||
.from('aset')
|
||||
.select('id, nama, kuantitas')
|
||||
.inFilter('id', asetIds)
|
||||
: [];
|
||||
debugPrint('[DEBUG] Aset data: ' + asetData.toString());
|
||||
final Map asetMap = {for (var a in asetData) a['id']: a};
|
||||
|
||||
// Prefill field controller
|
||||
nameController.text = paketData['nama']?.toString() ?? '';
|
||||
descriptionController.text = paketData['deskripsi']?.toString() ?? '';
|
||||
// Status mapping
|
||||
final statusDb =
|
||||
(paketData['status']?.toString().toLowerCase() ?? 'tersedia');
|
||||
selectedStatus.value =
|
||||
statusDb == 'pemeliharaan' ? 'Pemeliharaan' : 'Tersedia';
|
||||
|
||||
// Foto
|
||||
selectedImages.clear();
|
||||
if (fotoData.isNotEmpty) {
|
||||
for (var foto in fotoData) {
|
||||
final url = foto['foto_aset']?.toString();
|
||||
if (url != null && url.isNotEmpty) {
|
||||
selectedImages.add(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Item paket
|
||||
packageItems.clear();
|
||||
for (var item in paketItemData) {
|
||||
final aset = asetMap[item['aset_id']];
|
||||
packageItems.add({
|
||||
'asetId': item['aset_id'],
|
||||
'nama': aset != null ? aset['nama'] : '',
|
||||
'jumlah': item['kuantitas'],
|
||||
'stok': aset != null ? aset['kuantitas'] : 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Opsi waktu & harga sewa
|
||||
// Reset
|
||||
timeOptions['Per Jam']?.value = false;
|
||||
timeOptions['Per Hari']?.value = false;
|
||||
pricePerHourController.clear();
|
||||
maxHourController.clear();
|
||||
pricePerDayController.clear();
|
||||
maxDayController.clear();
|
||||
for (var sws in swsData) {
|
||||
final satuanNama =
|
||||
satuanWaktuMap[sws['satuan_waktu_id']]?.toString().toLowerCase() ??
|
||||
'';
|
||||
if (satuanNama.contains('jam')) {
|
||||
timeOptions['Per Jam']?.value = true;
|
||||
pricePerHourController.text = (sws['harga'] ?? '').toString();
|
||||
maxHourController.text = (sws['maksimal_waktu'] ?? '').toString();
|
||||
} else if (satuanNama.contains('hari')) {
|
||||
timeOptions['Per Hari']?.value = true;
|
||||
pricePerDayController.text = (sws['harga'] ?? '').toString();
|
||||
maxDayController.text = (sws['maksimal_waktu'] ?? '').toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Simpan snapshot initialFormData setelah prefill
|
||||
initialFormData = {
|
||||
'nama': nameController.text,
|
||||
'deskripsi': descriptionController.text,
|
||||
'status': selectedStatus.value,
|
||||
'images': List.from(selectedImages),
|
||||
'items': List.from(packageItems),
|
||||
'perJam': timeOptions['Per Jam']?.value ?? false,
|
||||
'perHari': timeOptions['Per Hari']?.value ?? false,
|
||||
'hargaJam': pricePerHourController.text,
|
||||
'maxJam': maxHourController.text,
|
||||
'hargaHari': pricePerDayController.text,
|
||||
'maxHari': maxDayController.text,
|
||||
};
|
||||
isFormChanged.value = false;
|
||||
} catch (e, st) {
|
||||
debugPrint('[ERROR] Gagal fetch paket detail: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pickImageFromCamera() async {
|
||||
try {
|
||||
final XFile? image = await _picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 80,
|
||||
maxWidth: 1024,
|
||||
maxHeight: 1024,
|
||||
);
|
||||
if (image != null) {
|
||||
selectedImages.add(image);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal mengambil gambar dari kamera: $e',
|
||||
snackPosition: SnackPosition.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();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_aset_controller.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
@ -23,26 +24,12 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = Get.find<PetugasAsetController>();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
|
||||
// Listen to tab changes and update controller
|
||||
_tabController.addListener(() {
|
||||
if (!_tabController.indexIsChanging) {
|
||||
controller.changeTab(_tabController.index);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to controller tab changes and update TabController
|
||||
ever(controller.selectedTabIndex, (index) {
|
||||
if (_tabController.index != index) {
|
||||
_tabController.animateTo(index);
|
||||
}
|
||||
});
|
||||
// Initialize with default tab (sewa)
|
||||
controller.changeTab(0);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -82,7 +69,7 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
body: Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
_buildTabBar(),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(child: _buildAssetList()),
|
||||
],
|
||||
),
|
||||
@ -93,7 +80,13 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
),
|
||||
),
|
||||
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,
|
||||
icon: Icon(Icons.add, color: AppColorsPetugas.blueGrotto),
|
||||
label: Text(
|
||||
@ -144,60 +137,19 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: AppColorsPetugas.textSecondary,
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicator: BoxDecoration(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
dividerColor: Colors.transparent,
|
||||
tabs: const [
|
||||
Tab(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.shopping_cart, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('Sewa', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.subscriptions, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Langganan',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// Tab bar has been removed as per requirements
|
||||
|
||||
Widget _buildAssetList() {
|
||||
return Obx(() {
|
||||
debugPrint('_buildAssetList: isLoading=${controller.isLoading.value}');
|
||||
debugPrint(
|
||||
'_buildAssetList: filteredAsetList length=${controller.filteredAsetList.length}',
|
||||
);
|
||||
if (controller.filteredAsetList.isNotEmpty) {
|
||||
debugPrint(
|
||||
'_buildAssetList: First item name=${controller.filteredAsetList[0]['nama']}',
|
||||
);
|
||||
}
|
||||
if (controller.isLoading.value) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
@ -255,10 +207,15 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredAsetList.length,
|
||||
itemCount: controller.filteredAsetList.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
final aset = controller.filteredAsetList[index];
|
||||
return _buildAssetCard(context, aset);
|
||||
if (index < controller.filteredAsetList.length) {
|
||||
final aset = controller.filteredAsetList[index];
|
||||
return _buildAssetCard(context, aset);
|
||||
} else {
|
||||
// Blank space at the end
|
||||
return const SizedBox(height: 80);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -266,7 +223,31 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
}
|
||||
|
||||
Widget _buildAssetCard(BuildContext context, Map<String, dynamic> aset) {
|
||||
final isAvailable = aset['tersedia'] == true;
|
||||
debugPrint('\n--- Building Asset Card ---');
|
||||
debugPrint('Asset data: $aset');
|
||||
|
||||
// Extract and validate all asset properties with proper null safety
|
||||
final status =
|
||||
aset['status']?.toString().toLowerCase() ?? 'tidak_diketahui';
|
||||
final isAvailable = status == 'tersedia';
|
||||
final imageUrl = aset['imageUrl']?.toString() ?? '';
|
||||
final harga =
|
||||
aset['harga'] is int
|
||||
? aset['harga'] as int
|
||||
: (int.tryParse(aset['harga']?.toString() ?? '0') ?? 0);
|
||||
final satuanWaktu =
|
||||
aset['satuan_waktu']?.toString().capitalizeFirst ?? 'Hari';
|
||||
final nama = aset['nama']?.toString().trim() ?? 'Nama tidak tersedia';
|
||||
final kategori = aset['kategori']?.toString().trim() ?? 'Umum';
|
||||
final orderId = aset['order_id']?.toString() ?? '';
|
||||
|
||||
// Debug prints for development
|
||||
debugPrint('Image URL: $imageUrl');
|
||||
debugPrint('Harga: $harga');
|
||||
debugPrint('Satuan Waktu: $satuanWaktu');
|
||||
debugPrint('Nama: $nama');
|
||||
debugPrint('Kategori: $kategori');
|
||||
debugPrint('Status: $status (Available: $isAvailable)');
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
@ -290,21 +271,46 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
child: Row(
|
||||
children: [
|
||||
// Asset image
|
||||
Container(
|
||||
SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
bottomLeft: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getAssetIcon(aset['kategori']),
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
size: 32,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder:
|
||||
(context, url) => Container(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getAssetIcon(
|
||||
kategori,
|
||||
), // Show category icon as placeholder
|
||||
color: AppColorsPetugas.navyBlue.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
errorWidget:
|
||||
(context, url, error) => Container(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons
|
||||
.broken_image, // Or your preferred error icon
|
||||
color: AppColorsPetugas.navyBlue.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -323,8 +329,8 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
aset['nama'],
|
||||
style: TextStyle(
|
||||
nama,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
@ -333,12 +339,63 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${controller.formatPrice(aset['harga'])} ${aset['satuan']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
// Harga dan satuan waktu (multi-line, tampilkan semua dari satuanWaktuSewa)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final satuanWaktuList =
|
||||
(aset['satuanWaktuSewa'] is List)
|
||||
? List<Map<String, dynamic>>.from(
|
||||
aset['satuanWaktuSewa'],
|
||||
)
|
||||
: [];
|
||||
final validSatuanWaktu =
|
||||
satuanWaktuList
|
||||
.where(
|
||||
(sw) =>
|
||||
(sw['harga'] ?? 0) > 0 &&
|
||||
(sw['nama_satuan_waktu'] !=
|
||||
null &&
|
||||
(sw['nama_satuan_waktu']
|
||||
as String)
|
||||
.isNotEmpty),
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (validSatuanWaktu.isNotEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children:
|
||||
validSatuanWaktu.map((sw) {
|
||||
final harga = sw['harga'] ?? 0;
|
||||
final satuan =
|
||||
sw['nama_satuan_waktu'] ?? '';
|
||||
return Text(
|
||||
'${controller.formatPrice(harga)} / $satuan',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
AppColorsPetugas
|
||||
.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
} else {
|
||||
// fallback: harga tunggal
|
||||
return Text(
|
||||
'${controller.formatPrice(aset['harga'] ?? 0)} / ${aset['satuan_waktu']?.toString().capitalizeFirst ?? 'Hari'}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -383,11 +440,36 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
children: [
|
||||
// Edit icon
|
||||
GestureDetector(
|
||||
onTap:
|
||||
() => _showAddEditAssetDialog(
|
||||
context,
|
||||
aset: aset,
|
||||
),
|
||||
onTap: () {
|
||||
// Navigate to PetugasTambahAsetView in edit mode with only the asset ID
|
||||
final assetId =
|
||||
aset['id']?.toString() ??
|
||||
''; // Changed from 'id_aset' to 'id'
|
||||
debugPrint(
|
||||
'[DEBUG] Navigating to edit asset with ID: $assetId',
|
||||
);
|
||||
debugPrint(
|
||||
'[DEBUG] Full asset data: $aset',
|
||||
); // Log full asset data for debugging
|
||||
|
||||
if (assetId.isEmpty) {
|
||||
debugPrint('[ERROR] Asset ID is empty!');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'ID Aset tidak valid',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_ASET,
|
||||
arguments: {
|
||||
'isEditing': true,
|
||||
'assetId': assetId,
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
|
@ -5,6 +5,7 @@ import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../../../utils/format_utils.dart';
|
||||
|
||||
class PetugasBumdesDashboardView
|
||||
extends GetView<PetugasBumdesDashboardController> {
|
||||
@ -23,12 +24,7 @@ class PetugasBumdesDashboardView
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => _showLogoutConfirmation(context),
|
||||
),
|
||||
],
|
||||
// actions: [],
|
||||
),
|
||||
drawer: PetugasSideNavbar(controller: controller),
|
||||
drawerEdgeDragWidth: 60,
|
||||
@ -118,8 +114,6 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
_buildRevenueStatistics(),
|
||||
const SizedBox(height: 16),
|
||||
_buildRevenueSources(),
|
||||
const SizedBox(height: 16),
|
||||
_buildRevenueTrend(),
|
||||
|
||||
// Add some padding at the bottom for better scrolling
|
||||
@ -156,25 +150,51 @@ class PetugasBumdesDashboardView
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
Obx(() {
|
||||
final avatar = controller.avatarUrl.value;
|
||||
if (avatar.isNotEmpty) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.network(
|
||||
avatar,
|
||||
width: 48,
|
||||
height: 48,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) => Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@ -208,15 +228,17 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Obx(
|
||||
() => Text(
|
||||
controller.userEmail.value,
|
||||
Obx(() {
|
||||
final name = controller.userName.value;
|
||||
final email = controller.userEmail.value;
|
||||
return Text(
|
||||
name.isNotEmpty ? name : email,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -642,19 +664,24 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Obx(
|
||||
() => Text(
|
||||
controller.totalPendapatanBulanIni.value,
|
||||
Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final total = stats['totalThisMonth'] ?? 0.0;
|
||||
return Text(
|
||||
formatRupiah(total),
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.success,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 6),
|
||||
Obx(
|
||||
() => Row(
|
||||
Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final percent = stats['percentComparedLast'] ?? 0.0;
|
||||
final isPositive = percent >= 0;
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@ -663,7 +690,7 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
controller.isKenaikanPositif.value
|
||||
isPositive
|
||||
? AppColorsPetugas.success.withOpacity(
|
||||
0.1,
|
||||
)
|
||||
@ -676,23 +703,23 @@ class PetugasBumdesDashboardView
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
controller.isKenaikanPositif.value
|
||||
isPositive
|
||||
? Icons.arrow_upward
|
||||
: Icons.arrow_downward,
|
||||
size: 14,
|
||||
color:
|
||||
controller.isKenaikanPositif.value
|
||||
isPositive
|
||||
? AppColorsPetugas.success
|
||||
: AppColorsPetugas.error,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
controller.persentaseKenaikan.value,
|
||||
'${percent.toStringAsFixed(1)}%',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color:
|
||||
controller.isKenaikanPositif.value
|
||||
isPositive
|
||||
? AppColorsPetugas.success
|
||||
: AppColorsPetugas.error,
|
||||
),
|
||||
@ -709,8 +736,8 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -747,12 +774,29 @@ class PetugasBumdesDashboardView
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildRevenueQuickInfo(
|
||||
'Pendapatan Sewa',
|
||||
controller.pendapatanSewa.value,
|
||||
AppColorsPetugas.navyBlue,
|
||||
Icons.shopping_cart_outlined,
|
||||
),
|
||||
child: Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final totalTunai = stats['totalTunai'] ?? 0.0;
|
||||
return _buildRevenueQuickInfo(
|
||||
'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() {
|
||||
final months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun'];
|
||||
|
||||
@ -912,6 +881,9 @@ class PetugasBumdesDashboardView
|
||||
child: Obx(() {
|
||||
// Get the trend data from controller
|
||||
final List<double> trendData = controller.trendPendapatan;
|
||||
if (trendData.isEmpty) {
|
||||
return Center(child: Text('Tidak ada data'));
|
||||
}
|
||||
final double maxValue = trendData.reduce(
|
||||
(curr, next) => curr > next ? curr : next,
|
||||
);
|
||||
@ -925,28 +897,28 @@ class PetugasBumdesDashboardView
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${maxValue.toStringAsFixed(1)}M',
|
||||
formatRupiah(maxValue),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(maxValue * 0.75).toStringAsFixed(1)}M',
|
||||
formatRupiah(maxValue * 0.75),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(maxValue * 0.5).toStringAsFixed(1)}M',
|
||||
formatRupiah(maxValue * 0.5),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(maxValue * 0.25).toStringAsFixed(1)}M',
|
||||
formatRupiah(maxValue * 0.25),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_paket_controller.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import 'package:bumrent_app/app/modules/petugas_bumdes/controllers/petugas_paket_controller.dart';
|
||||
import 'package:bumrent_app/app/routes/app_pages.dart';
|
||||
import 'package:bumrent_app/app/data/models/paket_model.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
|
||||
class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
const PetugasPaketView({Key? key}) : super(key: key);
|
||||
@ -53,7 +55,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET),
|
||||
onPressed:
|
||||
() => Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_PAKET,
|
||||
arguments: {'isEditing': false},
|
||||
),
|
||||
label: Text(
|
||||
'Tambah Paket',
|
||||
style: TextStyle(
|
||||
@ -115,7 +121,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.filteredPaketList.isEmpty) {
|
||||
if (controller.filteredPackages.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@ -136,7 +142,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET),
|
||||
onPressed:
|
||||
() => Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_PAKET,
|
||||
arguments: {'isEditing': false},
|
||||
),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Tambah Paket'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
@ -161,18 +171,192 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredPaketList.length,
|
||||
itemCount: controller.filteredPackages.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
final paket = controller.filteredPaketList[index];
|
||||
return _buildPaketCard(context, paket);
|
||||
if (index < controller.filteredPackages.length) {
|
||||
final paket = controller.filteredPackages[index];
|
||||
return _buildPaketCard(context, paket);
|
||||
} else {
|
||||
// Blank space at the end
|
||||
return const SizedBox(height: 80);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPaketCard(BuildContext context, Map<String, dynamic> paket) {
|
||||
final isAvailable = paket['tersedia'] == true;
|
||||
// Format price helper method
|
||||
String _formatPrice(dynamic price) {
|
||||
if (price == null) return '0';
|
||||
// If price is a string that can be parsed to a number
|
||||
if (price is String) {
|
||||
final number = double.tryParse(price) ?? 0;
|
||||
return number
|
||||
.toStringAsFixed(0)
|
||||
.replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]}.',
|
||||
);
|
||||
}
|
||||
// If price is already a number
|
||||
if (price is num) {
|
||||
return price
|
||||
.toStringAsFixed(0)
|
||||
.replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]}.',
|
||||
);
|
||||
}
|
||||
return '0';
|
||||
}
|
||||
|
||||
// Helper method to get time unit name based on ID
|
||||
String _getTimeUnitName(dynamic unitId) {
|
||||
if (unitId == null) return 'unit';
|
||||
|
||||
// Convert to string in case it's not already
|
||||
final unitIdStr = unitId.toString().toLowerCase();
|
||||
|
||||
// Map of known time unit IDs to their display names
|
||||
final timeUnitMap = {
|
||||
'6eaa32d9-855d-4214-b5b5-5c73d3edd9c5': 'jam',
|
||||
'582b7e66-6869-4495-9856-cef4a46683b0': 'hari',
|
||||
// Add more mappings as needed
|
||||
};
|
||||
|
||||
// If the unitId is a known ID, return the corresponding name
|
||||
if (timeUnitMap.containsKey(unitIdStr)) {
|
||||
return timeUnitMap[unitIdStr]!;
|
||||
}
|
||||
|
||||
// Check if the unit is already a name (like 'jam' or 'hari')
|
||||
final knownUnits = ['jam', 'hari', 'minggu', 'bulan'];
|
||||
if (knownUnits.contains(unitIdStr)) {
|
||||
return unitIdStr;
|
||||
}
|
||||
|
||||
// If the unit is a Map, try to extract the name from common fields
|
||||
if (unitId is Map) {
|
||||
return unitId['nama']?.toString().toLowerCase() ??
|
||||
unitId['name']?.toString().toLowerCase() ??
|
||||
unitId['satuan_waktu']?.toString().toLowerCase() ??
|
||||
'unit';
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 'unit';
|
||||
}
|
||||
|
||||
// Helper method to log time unit details
|
||||
void _logTimeUnitDetails(
|
||||
String packageName,
|
||||
List<Map<String, dynamic>> timeUnits,
|
||||
) {
|
||||
debugPrint('\n📦 [DEBUG] Package: $packageName');
|
||||
debugPrint('🔄 Found ${timeUnits.length} time units:');
|
||||
|
||||
for (var i = 0; i < timeUnits.length; i++) {
|
||||
final unit = timeUnits[i];
|
||||
debugPrint('\n ⏱️ Time Unit #${i + 1}:');
|
||||
|
||||
// Log all available keys and values
|
||||
debugPrint(' ├─ All fields: $unit');
|
||||
|
||||
// Log specific fields we're interested in
|
||||
unit.forEach((key, value) {
|
||||
debugPrint(' ├─ $key: $value (${value.runtimeType})');
|
||||
});
|
||||
|
||||
// Special handling for satuan_waktu if it's a map
|
||||
if (unit['satuan_waktu'] is Map) {
|
||||
final satuanWaktu = unit['satuan_waktu'] as Map;
|
||||
debugPrint(' └─ satuan_waktu details:');
|
||||
satuanWaktu.forEach((k, v) {
|
||||
debugPrint(' ├─ $k: $v (${v.runtimeType})');
|
||||
});
|
||||
}
|
||||
}
|
||||
debugPrint('\n');
|
||||
}
|
||||
|
||||
Widget _buildPaketCard(BuildContext context, dynamic paket) {
|
||||
// Handle both Map and PaketModel for backward compatibility
|
||||
final isPaketModel = paket is PaketModel;
|
||||
|
||||
debugPrint('\n🔍 [_buildPaketCard] Paket type: ${paket.runtimeType}');
|
||||
debugPrint('📋 Paket data: $paket');
|
||||
|
||||
// Extract status based on type
|
||||
final String status =
|
||||
isPaketModel
|
||||
? (paket.status?.toString().capitalizeFirst ?? 'Tidak Diketahui')
|
||||
: (paket['status']?.toString().capitalizeFirst ??
|
||||
'Tidak Diketahui');
|
||||
|
||||
debugPrint('🏷️ Extracted status: $status (isPaketModel: $isPaketModel)');
|
||||
|
||||
// Extract availability based on type
|
||||
final bool isAvailable =
|
||||
isPaketModel
|
||||
? (paket.kuantitas > 0)
|
||||
: ((paket['kuantitas'] as int?) ?? 0) > 0;
|
||||
|
||||
final String nama =
|
||||
isPaketModel
|
||||
? paket.nama
|
||||
: (paket['nama']?.toString() ?? 'Paket Tanpa Nama');
|
||||
|
||||
// Debug package info
|
||||
debugPrint('\n📦 [PACKAGE] ${paket.runtimeType} - $nama');
|
||||
debugPrint('├─ isPaketModel: $isPaketModel');
|
||||
debugPrint('├─ Available: $isAvailable');
|
||||
|
||||
// Get the first rental time unit price if available, otherwise use the base price
|
||||
final dynamic harga;
|
||||
if (isPaketModel) {
|
||||
if (paket.satuanWaktuSewa.isNotEmpty) {
|
||||
_logTimeUnitDetails(nama, paket.satuanWaktuSewa);
|
||||
|
||||
// Get the first time unit with its price
|
||||
final firstUnit = paket.satuanWaktuSewa.first;
|
||||
final firstUnitPrice = firstUnit['harga'];
|
||||
|
||||
debugPrint('💰 First time unit price: $firstUnitPrice');
|
||||
debugPrint('⏱️ First time unit ID: ${firstUnit['satuan_waktu_id']}');
|
||||
debugPrint('📝 First time unit details: $firstUnit');
|
||||
|
||||
// Always use the first time unit's price if available
|
||||
harga = firstUnitPrice ?? 0;
|
||||
} else {
|
||||
debugPrint('⚠️ No time units found for package: $nama');
|
||||
debugPrint('ℹ️ Using base price: ${paket.harga}');
|
||||
harga = paket.harga;
|
||||
}
|
||||
} else {
|
||||
// For non-PaketModel (Map) data
|
||||
if (isPaketModel && paket.satuanWaktuSewa.isNotEmpty) {
|
||||
final firstUnit = paket.satuanWaktuSewa.first;
|
||||
final firstUnitPrice = firstUnit['harga'];
|
||||
debugPrint('💰 [MAP] First time unit price: $firstUnitPrice');
|
||||
harga = firstUnitPrice ?? 0;
|
||||
} else {
|
||||
debugPrint('⚠️ [MAP] No time units found for package: $nama');
|
||||
debugPrint('ℹ️ [MAP] Using base price: ${paket['harga']}');
|
||||
harga = paket['harga'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('💵 Final price being used: $harga\n');
|
||||
|
||||
// Get the main photo URL
|
||||
final String? foto =
|
||||
isPaketModel
|
||||
? (paket.images?.isNotEmpty == true
|
||||
? paket.images!.first
|
||||
: paket.foto_paket)
|
||||
: (paket['foto_paket']?.toString() ??
|
||||
(paket['foto'] is String ? paket['foto'] : null));
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
@ -196,22 +380,83 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
child: Row(
|
||||
children: [
|
||||
// Paket image or icon
|
||||
Container(
|
||||
SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
bottomLeft: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getPaketIcon(paket['kategori']),
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
size: 32,
|
||||
),
|
||||
child:
|
||||
foto != null && foto.isNotEmpty
|
||||
? Image.network(
|
||||
foto,
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) => Container(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getPaketIcon(
|
||||
_getTimeUnitName(
|
||||
isPaketModel
|
||||
? (paket
|
||||
.satuanWaktuSewa
|
||||
.isNotEmpty
|
||||
? paket
|
||||
.satuanWaktuSewa
|
||||
.first['satuan_waktu_id'] ??
|
||||
'hari'
|
||||
: 'hari')
|
||||
: (paket['satuanWaktuSewa'] !=
|
||||
null &&
|
||||
paket['satuanWaktuSewa']
|
||||
.isNotEmpty
|
||||
? paket['satuanWaktuSewa'][0]['satuan_waktu_id']
|
||||
?.toString() ??
|
||||
'hari'
|
||||
: 'hari'),
|
||||
),
|
||||
),
|
||||
color: AppColorsPetugas.navyBlue
|
||||
.withOpacity(0.5),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getPaketIcon(
|
||||
_getTimeUnitName(
|
||||
isPaketModel
|
||||
? (paket.satuanWaktuSewa.isNotEmpty
|
||||
? paket
|
||||
.satuanWaktuSewa
|
||||
.first['satuan_waktu_id'] ??
|
||||
'hari'
|
||||
: 'hari')
|
||||
: (paket['satuanWaktuSewa'] != null &&
|
||||
paket['satuanWaktuSewa']
|
||||
.isNotEmpty
|
||||
? paket['satuanWaktuSewa'][0]['satuan_waktu_id']
|
||||
?.toString() ??
|
||||
'hari'
|
||||
: 'hari'),
|
||||
),
|
||||
),
|
||||
color: AppColorsPetugas.navyBlue.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -228,9 +473,10 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Package name
|
||||
Text(
|
||||
paket['nama'],
|
||||
style: TextStyle(
|
||||
nama,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
@ -239,13 +485,119 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Rp ${_formatPrice(paket['harga'])}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
// Prices with time units
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final List<Map<String, dynamic>> timeUnits =
|
||||
[];
|
||||
|
||||
// Get all time units
|
||||
if (isPaketModel &&
|
||||
paket.satuanWaktuSewa.isNotEmpty) {
|
||||
timeUnits.addAll(paket.satuanWaktuSewa);
|
||||
} else if (!isPaketModel &&
|
||||
paket['satuanWaktuSewa'] != null &&
|
||||
paket['satuanWaktuSewa'].isNotEmpty) {
|
||||
timeUnits.addAll(
|
||||
List<Map<String, dynamic>>.from(
|
||||
paket['satuanWaktuSewa'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// If no time units, show nothing
|
||||
if (timeUnits.isEmpty)
|
||||
return const SizedBox.shrink();
|
||||
|
||||
// Filter out time units with price 0 or null
|
||||
final validTimeUnits =
|
||||
timeUnits.where((unit) {
|
||||
final price =
|
||||
unit['harga'] is int
|
||||
? unit['harga']
|
||||
: int.tryParse(
|
||||
unit['harga']
|
||||
?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0;
|
||||
return price > 0;
|
||||
}).toList();
|
||||
|
||||
if (validTimeUnits.isEmpty)
|
||||
return const SizedBox.shrink();
|
||||
|
||||
return Column(
|
||||
children:
|
||||
validTimeUnits
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) {
|
||||
final index = entry.key;
|
||||
final unit = entry.value;
|
||||
final unitPrice =
|
||||
unit['harga'] is int
|
||||
? unit['harga']
|
||||
: int.tryParse(
|
||||
unit['harga']
|
||||
?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0;
|
||||
final unitName = _getTimeUnitName(
|
||||
unit['satuan_waktu_id'],
|
||||
);
|
||||
final isFirst = index == 0;
|
||||
|
||||
if (unitPrice <= 0)
|
||||
return const SizedBox.shrink();
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Rp ${_formatPrice(unitPrice)}/$unitName',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
AppColorsPetugas
|
||||
.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
})
|
||||
.where(
|
||||
(widget) => widget is! SizedBox,
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!isPaketModel &&
|
||||
paket['harga'] != null &&
|
||||
(paket['harga'] is int
|
||||
? paket['harga']
|
||||
: int.tryParse(
|
||||
paket['harga']?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0) >
|
||||
0) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Rp ${_formatPrice(paket['harga'])}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -258,25 +610,31 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isAvailable
|
||||
status.toLowerCase() == 'tersedia'
|
||||
? AppColorsPetugas.successLight
|
||||
: status.toLowerCase() == 'pemeliharaan'
|
||||
? AppColorsPetugas.warningLight
|
||||
: AppColorsPetugas.errorLight,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color:
|
||||
isAvailable
|
||||
status.toLowerCase() == 'tersedia'
|
||||
? AppColorsPetugas.success
|
||||
: status.toLowerCase() == 'pemeliharaan'
|
||||
? AppColorsPetugas.warning
|
||||
: AppColorsPetugas.error,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
isAvailable ? 'Aktif' : 'Nonaktif',
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color:
|
||||
isAvailable
|
||||
status.toLowerCase() == 'tersedia'
|
||||
? AppColorsPetugas.success
|
||||
: status.toLowerCase() == 'pemeliharaan'
|
||||
? AppColorsPetugas.warning
|
||||
: AppColorsPetugas.error,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
@ -290,9 +648,12 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
// Edit icon
|
||||
GestureDetector(
|
||||
onTap:
|
||||
() => _showAddEditPaketDialog(
|
||||
context,
|
||||
paket: paket,
|
||||
() => Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_PAKET,
|
||||
arguments: {
|
||||
'isEditing': true,
|
||||
'paket': paket,
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
@ -350,33 +711,42 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPrice(dynamic price) {
|
||||
if (price == null) return '0';
|
||||
|
||||
// Convert the price to string and handle formatting
|
||||
String priceStr = price.toString();
|
||||
|
||||
// Add thousand separators
|
||||
final RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))');
|
||||
String formatted = priceStr.replaceAllMapped(reg, (Match m) => '${m[1]}.');
|
||||
|
||||
return formatted;
|
||||
// Add this helper method to get color based on status
|
||||
Color _getStatusColor(String status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'aktif':
|
||||
return AppColorsPetugas.success;
|
||||
case 'tidak aktif':
|
||||
case 'nonaktif':
|
||||
return AppColorsPetugas.error;
|
||||
case 'dalam perbaikan':
|
||||
case 'maintenance':
|
||||
return AppColorsPetugas.warning;
|
||||
case 'tersedia':
|
||||
return AppColorsPetugas.success;
|
||||
case 'pemeliharaan':
|
||||
return AppColorsPetugas.warning;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getPaketIcon(String? category) {
|
||||
if (category == null) return Icons.category;
|
||||
IconData _getPaketIcon(String? timeUnit) {
|
||||
if (timeUnit == null) return Icons.access_time;
|
||||
|
||||
switch (category.toLowerCase()) {
|
||||
case 'bulanan':
|
||||
return Icons.calendar_month;
|
||||
case 'tahunan':
|
||||
switch (timeUnit.toLowerCase()) {
|
||||
case 'jam':
|
||||
return Icons.access_time;
|
||||
case 'hari':
|
||||
return Icons.calendar_today;
|
||||
case 'premium':
|
||||
return Icons.star;
|
||||
case 'bisnis':
|
||||
return Icons.business;
|
||||
case 'minggu':
|
||||
return Icons.date_range;
|
||||
case 'bulan':
|
||||
return Icons.calendar_month;
|
||||
case 'tahun':
|
||||
return Icons.calendar_view_month;
|
||||
default:
|
||||
return Icons.category;
|
||||
return Icons.access_time;
|
||||
}
|
||||
}
|
||||
|
||||
@ -426,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(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@ -448,7 +838,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
paket['nama'],
|
||||
nama,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -473,16 +863,15 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDetailItem('Kategori', paket['kategori']),
|
||||
_buildDetailItem(
|
||||
'Harga',
|
||||
controller.formatPrice(paket['harga']),
|
||||
'Rp ${_formatPrice(harga)}',
|
||||
),
|
||||
_buildDetailItem(
|
||||
'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(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: paket['items'].length,
|
||||
itemCount: items.length,
|
||||
separatorBuilder:
|
||||
(context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final item = paket['items'][index];
|
||||
final item = items[index];
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColorsPetugas.babyBlue,
|
||||
@ -601,10 +990,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddEditPaketDialog(
|
||||
BuildContext context, {
|
||||
Map<String, dynamic>? paket,
|
||||
}) {
|
||||
void _showAddEditPaketDialog(BuildContext context, {dynamic paket}) {
|
||||
// Handle both Map and PaketModel for backward compatibility
|
||||
final isPaketModel = paket is PaketModel;
|
||||
final String? id = isPaketModel ? paket.id : paket?['id'];
|
||||
final String title = id == null ? 'Tambah Paket' : 'Edit Paket';
|
||||
final isEditing = paket != null;
|
||||
|
||||
// This would be implemented with proper form validation in a real app
|
||||
@ -613,7 +1003,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
isEditing ? 'Edit Paket' : 'Tambah Paket Baru',
|
||||
title,
|
||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
content: const Text(
|
||||
@ -652,10 +1042,13 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmation(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> paket,
|
||||
) {
|
||||
void _showDeleteConfirmation(BuildContext context, dynamic paket) {
|
||||
// Handle both Map and PaketModel for backward compatibility
|
||||
final isPaketModel = paket is PaketModel;
|
||||
final String id = isPaketModel ? paket.id : (paket['id']?.toString() ?? '');
|
||||
final String nama =
|
||||
isPaketModel ? paket.nama : (paket['nama']?.toString() ?? 'Paket');
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
@ -664,9 +1057,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
'Konfirmasi Hapus',
|
||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
content: Text(
|
||||
'Apakah Anda yakin ingin menghapus paket "${paket['nama']}"?',
|
||||
),
|
||||
content: Text('Apakah Anda yakin ingin menghapus paket "$nama"?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
@ -678,7 +1069,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
controller.deletePaket(paket['id']);
|
||||
controller.deletePaket(id);
|
||||
Get.snackbar(
|
||||
'Paket Dihapus',
|
||||
'Paket berhasil dihapus dari sistem',
|
||||
|
@ -6,6 +6,7 @@ import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import 'petugas_detail_sewa_view.dart';
|
||||
import '../../../data/models/rental_booking_model.dart';
|
||||
|
||||
class PetugasSewaView extends StatefulWidget {
|
||||
const PetugasSewaView({Key? key}) : super(key: key);
|
||||
@ -160,6 +161,10 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
}
|
||||
|
||||
Widget _buildSearchSection() {
|
||||
// Tambahkan controller untuk TextField agar bisa dikosongkan
|
||||
final TextEditingController searchController = TextEditingController(
|
||||
text: controller.searchQuery.value,
|
||||
);
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
@ -173,9 +178,9 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
onChanged: (value) {
|
||||
controller.setSearchQuery(value);
|
||||
controller.setOrderIdQuery(value);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari nama warga atau ID pesanan...',
|
||||
@ -204,10 +209,21 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true,
|
||||
suffixIcon: Icon(
|
||||
Icons.tune_rounded,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
size: 20,
|
||||
suffixIcon: Obx(
|
||||
() =>
|
||||
controller.searchQuery.value.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
controller.setSearchQuery('');
|
||||
},
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -241,17 +257,44 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
final filteredList =
|
||||
status == 'Semua'
|
||||
? controller.filteredSewaList
|
||||
: status == 'Menunggu Pembayaran'
|
||||
? controller.sewaList
|
||||
.where(
|
||||
(sewa) =>
|
||||
sewa.status.toUpperCase() == 'MENUNGGU PEMBAYARAN' ||
|
||||
sewa.status.toUpperCase() == 'PEMBAYARAN DENDA',
|
||||
)
|
||||
.toList()
|
||||
: status == 'Periksa Pembayaran'
|
||||
? controller.sewaList
|
||||
.where(
|
||||
(sewa) =>
|
||||
sewa['status'] == 'Periksa Pembayaran' ||
|
||||
sewa['status'] == 'Pembayaran Denda' ||
|
||||
sewa['status'] == 'Periksa Denda',
|
||||
sewa.status.toUpperCase() == 'PERIKSA PEMBAYARAN' ||
|
||||
sewa.status.toUpperCase() == 'PERIKSA PEMBAYARAN DENDA',
|
||||
)
|
||||
.toList()
|
||||
: status == 'Diterima'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'DITERIMA')
|
||||
.toList()
|
||||
: status == 'Aktif'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'AKTIF')
|
||||
.toList()
|
||||
: status == 'Dikembalikan'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'DIKEMBALIKAN')
|
||||
.toList()
|
||||
: status == 'Selesai'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'SELESAI')
|
||||
.toList()
|
||||
: status == 'Dibatalkan'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'DIBATALKAN')
|
||||
.toList()
|
||||
: controller.sewaList
|
||||
.where((sewa) => sewa['status'] == status)
|
||||
.where((sewa) => sewa.status == status)
|
||||
.toList();
|
||||
|
||||
if (filteredList.isEmpty) {
|
||||
@ -313,40 +356,25 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildSewaCard(BuildContext context, Map<String, dynamic> sewa) {
|
||||
final statusColor = controller.getStatusColor(sewa['status']);
|
||||
final status = sewa['status'];
|
||||
Widget _buildSewaCard(BuildContext context, SewaModel sewa) {
|
||||
final statusColor = controller.getStatusColor(sewa.status);
|
||||
final status = sewa.status;
|
||||
|
||||
// Get appropriate icon for status
|
||||
IconData statusIcon;
|
||||
switch (status) {
|
||||
case 'Menunggu Pembayaran':
|
||||
statusIcon = Icons.payments_outlined;
|
||||
break;
|
||||
case 'Periksa Pembayaran':
|
||||
statusIcon = Icons.fact_check_outlined;
|
||||
break;
|
||||
case 'Diterima':
|
||||
statusIcon = Icons.check_circle_outlined;
|
||||
break;
|
||||
case 'Pembayaran Denda':
|
||||
statusIcon = Icons.money_off_csred_outlined;
|
||||
break;
|
||||
case 'Periksa Denda':
|
||||
statusIcon = Icons.assignment_late_outlined;
|
||||
break;
|
||||
case 'Dikembalikan':
|
||||
statusIcon = Icons.assignment_return_outlined;
|
||||
break;
|
||||
case 'Selesai':
|
||||
statusIcon = Icons.task_alt_outlined;
|
||||
break;
|
||||
case 'Dibatalkan':
|
||||
statusIcon = Icons.cancel_outlined;
|
||||
break;
|
||||
default:
|
||||
statusIcon = Icons.help_outline_rounded;
|
||||
}
|
||||
IconData statusIcon = controller.getStatusIcon(status);
|
||||
|
||||
// Flag untuk membedakan tipe pesanan
|
||||
final bool isAset = sewa.tipePesanan == 'tunggal';
|
||||
final bool isPaket = sewa.tipePesanan == 'paket';
|
||||
|
||||
// Pilih nama aset/paket
|
||||
final String namaAsetAtauPaket =
|
||||
isAset
|
||||
? (sewa.asetNama ?? '-')
|
||||
: (isPaket ? (sewa.paketNama ?? '-') : '-');
|
||||
// Pilih foto aset/paket jika ingin digunakan
|
||||
final String? fotoAsetAtauPaket =
|
||||
isAset ? sewa.asetFoto : (isPaket ? sewa.paketFoto : null);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
@ -370,6 +398,35 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Status header inside the card
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.12),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(statusIcon, size: 16, color: statusColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
|
||||
child: Row(
|
||||
@ -378,14 +435,22 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||
child: Text(
|
||||
sewa['nama_warga'].substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
backgroundImage:
|
||||
(sewa.wargaAvatar != null &&
|
||||
sewa.wargaAvatar.isNotEmpty)
|
||||
? NetworkImage(sewa.wargaAvatar)
|
||||
: null,
|
||||
child:
|
||||
(sewa.wargaAvatar == null || sewa.wargaAvatar.isEmpty)
|
||||
? Text(
|
||||
sewa.wargaNama.substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
@ -395,55 +460,22 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sewa['nama_warga'],
|
||||
sewa.wargaNama,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
statusIcon,
|
||||
size: 12,
|
||||
color: statusColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'#${sewa['order_id']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
'Tanggal Pesan: ' +
|
||||
(sewa.tanggalPemesanan != null
|
||||
? '${sewa.tanggalPemesanan.day.toString().padLeft(2, '0')}-${sewa.tanggalPemesanan.month.toString().padLeft(2, '0')}-${sewa.tanggalPemesanan.year}'
|
||||
: '-'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -460,7 +492,7 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
controller.formatPrice(sewa['total_biaya']),
|
||||
controller.formatPrice(sewa.totalTagihan),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -481,33 +513,51 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
child: Divider(height: 1, color: Colors.grey.shade200),
|
||||
),
|
||||
|
||||
// Asset details
|
||||
// Asset/Paket details
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
// Asset icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
// Asset/Paket image or icon
|
||||
if (fotoAsetAtauPaket != null &&
|
||||
fotoAsetAtauPaket.isNotEmpty)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Image.network(
|
||||
fotoAsetAtauPaket,
|
||||
width: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) => Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 28,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 20,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 20,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Asset name and duration
|
||||
// Asset/Paket name and duration
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sewa['nama_aset'],
|
||||
namaAsetAtauPaket,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
@ -524,7 +574,7 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${sewa['tanggal_mulai']} - ${sewa['tanggal_selesai']}',
|
||||
'${sewa.waktuMulai.toIso8601String().substring(0, 10)} - ${sewa.waktuSelesai.toIso8601String().substring(0, 10)}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
@ -9,32 +10,51 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Tambah Aset',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [_buildHeaderSection(), _buildFormSection(context)],
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Obx(() => Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
controller.isEditing.value ? 'Edit Aset' : 'Tambah Aset',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: _buildBottomBar(),
|
||||
body: Stack(
|
||||
children: [
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeaderSection(),
|
||||
_buildFormSection(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.isLoading.value)
|
||||
Container(
|
||||
color: Colors.black54,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColorsPetugas.blueGrotto),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: _buildBottomBar(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.only(top: 10, left: 20, right: 20, bottom: 5), // Reduced padding
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto],
|
||||
@ -42,50 +62,8 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Informasi Aset Baru',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Isi data dengan lengkap untuk menambahkan aset',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: Container(
|
||||
height: 12, // Further reduced height
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -131,69 +109,36 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
_buildImageUploader(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Category Section
|
||||
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'),
|
||||
// Status Section
|
||||
_buildSectionHeader(icon: Icons.check_circle, title: 'Status'),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Category and Status as cards
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Kategori',
|
||||
options: controller.categoryOptions,
|
||||
selectedOption: controller.selectedCategory,
|
||||
onChanged: controller.setCategory,
|
||||
icon: Icons.inventory_2,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Status',
|
||||
options: controller.statusOptions,
|
||||
selectedOption: controller.selectedStatus,
|
||||
onChanged: controller.setStatus,
|
||||
icon: Icons.check_circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
// Status card
|
||||
_buildCategorySelect(
|
||||
title: 'Status',
|
||||
options: controller.statusOptions,
|
||||
selectedOption: controller.selectedStatus,
|
||||
onChanged: controller.setStatus,
|
||||
icon: Icons.check_circle,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Quantity Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.format_list_numbered,
|
||||
title: 'Kuantitas & Pengukuran',
|
||||
title: 'Kuantitas',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quantity fields
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildTextField(
|
||||
label: 'Kuantitas',
|
||||
hint: 'Jumlah aset',
|
||||
controller: controller.quantityController,
|
||||
isRequired: true,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
prefixIcon: Icons.numbers,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: _buildTextField(
|
||||
label: 'Satuan Ukur',
|
||||
hint: 'contoh: Unit, Buah',
|
||||
controller: controller.unitOfMeasureController,
|
||||
prefixIcon: Icons.straighten,
|
||||
),
|
||||
),
|
||||
],
|
||||
// Quantity field
|
||||
_buildTextField(
|
||||
label: 'Kuantitas',
|
||||
hint: 'Jumlah aset',
|
||||
controller: controller.quantityController,
|
||||
isRequired: true,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
prefixIcon: Icons.numbers,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@ -654,6 +599,114 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
);
|
||||
}
|
||||
|
||||
// Show image source options
|
||||
void _showImageSourceOptions() {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Pilih Sumber Gambar',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildImageSourceOption(
|
||||
icon: Icons.camera_alt,
|
||||
label: 'Kamera',
|
||||
onTap: () {
|
||||
Get.back();
|
||||
controller.pickImageFromCamera();
|
||||
},
|
||||
),
|
||||
_buildImageSourceOption(
|
||||
icon: Icons.photo_library,
|
||||
label: 'Galeri',
|
||||
onTap: () {
|
||||
Get.back();
|
||||
controller.pickImageFromGallery();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageSourceOption({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 30,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageUploader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@ -696,7 +749,7 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
children: [
|
||||
// Add button
|
||||
GestureDetector(
|
||||
onTap: () => controller.addSampleImage(),
|
||||
onTap: _showImageSourceOptions,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
@ -732,69 +785,107 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
),
|
||||
|
||||
// Image previews
|
||||
...controller.selectedImages.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
...List<Widget>.generate(
|
||||
controller.selectedImages.length,
|
||||
(index) => Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: Obx(
|
||||
() {
|
||||
// Check if we have a network URL for this index
|
||||
if (index < controller.networkImageUrls.length &&
|
||||
controller.networkImageUrls[index].isNotEmpty) {
|
||||
// Display network image
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
controller.networkImageUrls[index],
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Center(
|
||||
child: Icon(Icons.error_outline, color: Colors.red),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Display local file
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: FutureBuilder<File>(
|
||||
future: File(controller.selectedImages[index].path).exists().then((exists) {
|
||||
if (exists) {
|
||||
return File(controller.selectedImages[index].path);
|
||||
} else {
|
||||
return File(controller.selectedImages[index].path);
|
||||
}
|
||||
}),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data != null) {
|
||||
return Image.file(
|
||||
snapshot.data!,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
color: Colors.grey[200],
|
||||
child: const Icon(Icons.broken_image, color: Colors.grey),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
color: Colors.grey[200],
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.removeImage(index),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 40,
|
||||
),
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.removeImage(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: AppColorsPetugas.error,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -850,7 +941,9 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.save),
|
||||
label: Text(isSubmitting ? 'Menyimpan...' : 'Simpan Aset'),
|
||||
label: Obx(() => Text(
|
||||
isSubmitting ? 'Menyimpan...' : (controller.isEditing.value ? 'Simpan Perubahan' : 'Simpan Aset'),
|
||||
)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../controllers/petugas_tambah_paket_controller.dart';
|
||||
import 'dart:io';
|
||||
|
||||
class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
const PetugasTambahPaketView({Key? key}) : super(key: key);
|
||||
@ -12,9 +13,11 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Tambah Paket',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
title: Obx(
|
||||
() => Text(
|
||||
controller.isEditing.value ? 'Edit Paket' : 'Tambah Paket',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
elevation: 0,
|
||||
@ -24,7 +27,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
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) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
@ -132,22 +77,22 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Category Section
|
||||
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'),
|
||||
_buildSectionHeader(icon: Icons.category, title: 'Status'),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Category and Status as cards
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Kategori',
|
||||
options: controller.categoryOptions,
|
||||
selectedOption: controller.selectedCategory,
|
||||
onChanged: controller.setCategory,
|
||||
icon: Icons.category,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Expanded(
|
||||
// child: _buildCategorySelect(
|
||||
// title: 'Kategori',
|
||||
// options: controller.categoryOptions,
|
||||
// selectedOption: controller.selectedCategory,
|
||||
// onChanged: controller.setCategory,
|
||||
// icon: Icons.category,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Status',
|
||||
@ -161,24 +106,6 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Price Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.monetization_on,
|
||||
title: 'Harga Paket',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
label: 'Harga Paket',
|
||||
hint: 'Masukkan harga paket',
|
||||
controller: controller.priceController,
|
||||
isRequired: true,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
prefixText: 'Rp ',
|
||||
prefixIcon: Icons.payments,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Package Items Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.inventory_2,
|
||||
@ -186,6 +113,40 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_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),
|
||||
],
|
||||
),
|
||||
@ -310,7 +271,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Asset dropdown
|
||||
DropdownButtonFormField<int>(
|
||||
DropdownButtonFormField<String>(
|
||||
value: controller.selectedAsset.value,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pilih Aset',
|
||||
@ -319,8 +280,8 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
hint: const Text('Pilih Aset'),
|
||||
items:
|
||||
controller.availableAssets.map((asset) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: asset['id'] as int,
|
||||
return DropdownMenuItem<String>(
|
||||
value: asset['id'].toString(),
|
||||
child: Text(
|
||||
'${asset['nama']} (Stok: ${asset['stok']})',
|
||||
),
|
||||
@ -422,7 +383,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Asset dropdown
|
||||
DropdownButtonFormField<int>(
|
||||
DropdownButtonFormField<String>(
|
||||
value: controller.selectedAsset.value,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pilih Aset',
|
||||
@ -431,8 +392,8 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
hint: const Text('Pilih Aset'),
|
||||
items:
|
||||
controller.availableAssets.map((asset) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: asset['id'] as int,
|
||||
return DropdownMenuItem<String>(
|
||||
value: asset['id'].toString(),
|
||||
child: Text(
|
||||
'${asset['nama']} (Stok: ${asset['stok']})',
|
||||
),
|
||||
@ -757,7 +718,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
children: [
|
||||
// Add button
|
||||
GestureDetector(
|
||||
onTap: () => controller.addSampleImage(),
|
||||
onTap: _showImageSourceOptions,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
@ -791,69 +752,82 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Image previews
|
||||
...controller.selectedImages.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
...List<Widget>.generate(controller.selectedImages.length, (
|
||||
index,
|
||||
) {
|
||||
final img = controller.selectedImages[index];
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.removeImage(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child:
|
||||
(img is String && img.startsWith('http'))
|
||||
? Image.network(
|
||||
img,
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
const Center(
|
||||
child: Icon(
|
||||
Icons.broken_image,
|
||||
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(
|
||||
Icons.close,
|
||||
color: AppColorsPetugas.error,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
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() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
@ -899,26 +971,37 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
final isSubmitting = controller.isSubmitting.value;
|
||||
return ElevatedButton.icon(
|
||||
onPressed:
|
||||
isValid && !isSubmitting ? controller.savePaket : null,
|
||||
controller.isFormChanged.value && !isSubmitting
|
||||
? controller.savePaket
|
||||
: null,
|
||||
icon:
|
||||
isSubmitting
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.save),
|
||||
label: Text(isSubmitting ? 'Menyimpan...' : 'Simpan Paket'),
|
||||
label: Text(
|
||||
isSubmitting
|
||||
? 'Menyimpan...'
|
||||
: (controller.isEditing.value
|
||||
? 'Simpan Paket'
|
||||
: 'Tambah Paket'),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
|
||||
class PetugasSideNavbar extends StatelessWidget {
|
||||
@ -11,7 +12,7 @@ class PetugasSideNavbar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
@ -32,24 +33,46 @@ class PetugasSideNavbar extends StatelessWidget {
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
|
||||
color: AppColors.primary,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.white,
|
||||
child: Icon(Icons.person, color: AppColors.primary, size: 36),
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
final avatar = controller.avatarUrl.value;
|
||||
if (avatar.isNotEmpty) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundImage: NetworkImage(avatar),
|
||||
onBackgroundImageError: (error, stackTrace) {},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.white,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: AppColors.primary,
|
||||
size: 36,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
|
@ -34,7 +34,7 @@ class SplashView extends GetView<SplashController> {
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/pattern.png'),
|
||||
image: AssetImage('assets/images/logo.png'), // Using logo.png which exists
|
||||
repeat: ImageRepeat.repeat,
|
||||
scale: 4.0,
|
||||
),
|
||||
|
@ -8,12 +8,6 @@ import '../../../data/providers/aset_provider.dart';
|
||||
class WargaSewaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Ensure NavigationService is registered and set to Sewa tab
|
||||
if (Get.isRegistered<NavigationService>()) {
|
||||
final navService = Get.find<NavigationService>();
|
||||
navService.setNavIndex(1); // Set to Sewa tab
|
||||
}
|
||||
|
||||
// Ensure AuthProvider is registered
|
||||
if (!Get.isRegistered<AuthProvider>()) {
|
||||
Get.put(AuthProvider(), permanent: true);
|
||||
|
@ -47,17 +47,21 @@ class PembayaranSewaController extends GetxController
|
||||
final isLoading = false.obs;
|
||||
final currentStep = 0.obs;
|
||||
|
||||
// Payment proof images - now a list to support multiple images (both File and WebImageFile)
|
||||
final RxList<dynamic> paymentProofImages = <dynamic>[].obs;
|
||||
// Payment proof images for tagihan awal
|
||||
final RxList<dynamic> paymentProofImagesTagihanAwal = <dynamic>[].obs;
|
||||
// Payment proof images for denda
|
||||
final RxList<dynamic> paymentProofImagesDenda = <dynamic>[].obs;
|
||||
|
||||
// Track original images loaded from database
|
||||
final RxList<WebImageFile> originalImages = <WebImageFile>[].obs;
|
||||
|
||||
// Track images marked for deletion
|
||||
final RxList<WebImageFile> imagesToDelete = <WebImageFile>[].obs;
|
||||
final RxList<WebImageFile> imagesToDeleteTagihanAwal = <WebImageFile>[].obs;
|
||||
final RxList<WebImageFile> imagesToDeleteDenda = <WebImageFile>[].obs;
|
||||
|
||||
// Flag to track if there are changes that need to be saved
|
||||
final RxBool hasUnsavedChanges = false.obs;
|
||||
final RxBool hasUnsavedChangesTagihanAwal = false.obs;
|
||||
final RxBool hasUnsavedChangesDenda = false.obs;
|
||||
|
||||
// Get image widget for a specific image
|
||||
Widget getImageWidget(dynamic imageFile) {
|
||||
@ -98,12 +102,7 @@ class PembayaranSewaController extends GetxController
|
||||
}
|
||||
// For mobile with a File object
|
||||
else if (imageFile is File) {
|
||||
return Image.file(
|
||||
imageFile,
|
||||
height: 120,
|
||||
width: 120,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
return Image.file(imageFile, height: 120, width: 120, fit: BoxFit.cover);
|
||||
}
|
||||
// Fallback for any other type
|
||||
else {
|
||||
@ -118,18 +117,26 @@ class PembayaranSewaController extends GetxController
|
||||
|
||||
// Remove an image from the list
|
||||
void removeImage(dynamic image) {
|
||||
// If this is an existing image (WebImageFile), add it to imagesToDelete
|
||||
if (image is WebImageFile && image.id.isNotEmpty) {
|
||||
imagesToDelete.add(image);
|
||||
debugPrint('🗑️ Marked image for deletion: ${image.imageUrl} (ID: ${image.id})');
|
||||
if (selectedPaymentType.value == 'denda') {
|
||||
// Untuk denda
|
||||
if (image is WebImageFile && image.id.isNotEmpty) {
|
||||
imagesToDeleteDenda.add(image);
|
||||
debugPrint(
|
||||
'🗑️ Marked image for deletion (denda): \\${image.imageUrl} (ID: \\${image.id})',
|
||||
);
|
||||
}
|
||||
paymentProofImagesDenda.remove(image);
|
||||
} else {
|
||||
// Default/tagihan awal
|
||||
if (image is WebImageFile && image.id.isNotEmpty) {
|
||||
imagesToDeleteTagihanAwal.add(image);
|
||||
debugPrint(
|
||||
'🗑️ Marked image for deletion: \\${image.imageUrl} (ID: \\${image.id})',
|
||||
);
|
||||
}
|
||||
paymentProofImagesTagihanAwal.remove(image);
|
||||
}
|
||||
|
||||
// Remove from the current list
|
||||
paymentProofImages.remove(image);
|
||||
|
||||
// Check if we have any changes (additions or deletions)
|
||||
_checkForChanges();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
@ -161,22 +168,25 @@ class PembayaranSewaController extends GetxController
|
||||
panEnabled: true,
|
||||
minScale: 0.5,
|
||||
maxScale: 4,
|
||||
child: kIsWeb
|
||||
? Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.contain,
|
||||
height: Get.height,
|
||||
width: Get.width,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Center(child: Text('Error loading image'));
|
||||
},
|
||||
)
|
||||
: Image.file(
|
||||
File(imageUrl),
|
||||
fit: BoxFit.contain,
|
||||
height: Get.height,
|
||||
width: Get.width,
|
||||
),
|
||||
child:
|
||||
kIsWeb
|
||||
? Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.contain,
|
||||
height: Get.height,
|
||||
width: Get.width,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Center(
|
||||
child: Text('Error loading image'),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Image.file(
|
||||
File(imageUrl),
|
||||
fit: BoxFit.contain,
|
||||
height: Get.height,
|
||||
width: Get.width,
|
||||
),
|
||||
),
|
||||
// Close button
|
||||
Positioned(
|
||||
@ -196,35 +206,33 @@ class PembayaranSewaController extends GetxController
|
||||
|
||||
// Check if there are any changes to save (new images added or existing images removed)
|
||||
void _checkForChanges() {
|
||||
// We have changes if:
|
||||
// 1. We have images marked for deletion
|
||||
// 2. We have new images (files) added
|
||||
// 3. The current list differs from the original list
|
||||
|
||||
bool hasChanges = false;
|
||||
|
||||
// Check if any images are marked for deletion
|
||||
if (imagesToDelete.isNotEmpty) {
|
||||
hasChanges = true;
|
||||
bool hasChangesTagihanAwal = false;
|
||||
bool hasChangesDenda = false;
|
||||
if (imagesToDeleteTagihanAwal.isNotEmpty) {
|
||||
hasChangesTagihanAwal = true;
|
||||
}
|
||||
|
||||
// Check if any new images have been added
|
||||
for (dynamic image in paymentProofImages) {
|
||||
if (imagesToDeleteDenda.isNotEmpty) {
|
||||
hasChangesDenda = true;
|
||||
}
|
||||
for (dynamic image in paymentProofImagesTagihanAwal) {
|
||||
if (image is File) {
|
||||
// This is a new image
|
||||
hasChanges = true;
|
||||
hasChangesTagihanAwal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the number of images has changed
|
||||
if (paymentProofImages.length != originalImages.length) {
|
||||
hasChanges = true;
|
||||
for (dynamic image in paymentProofImagesDenda) {
|
||||
if (image is File) {
|
||||
hasChangesDenda = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
hasUnsavedChanges.value = hasChanges;
|
||||
debugPrint('💾 Has unsaved changes: $hasChanges');
|
||||
hasUnsavedChangesTagihanAwal.value = hasChangesTagihanAwal;
|
||||
hasUnsavedChangesDenda.value = hasChangesDenda;
|
||||
debugPrint(
|
||||
'💾 Has unsaved changes (tagihan awal): $hasChangesTagihanAwal, (denda): $hasChangesDenda',
|
||||
);
|
||||
}
|
||||
|
||||
final isUploading = false.obs;
|
||||
final uploadProgress = 0.0.obs;
|
||||
|
||||
@ -260,8 +268,16 @@ class PembayaranSewaController extends GetxController
|
||||
'rental_period': rentalData['waktuSewa'] ?? '',
|
||||
'duration': rentalData['duration'] ?? '',
|
||||
'price_per_unit': 0, // This might not be available in rental data
|
||||
'total_price': rentalData['totalPrice'] != null ?
|
||||
int.tryParse(rentalData['totalPrice'].toString().replaceAll(RegExp(r'[^0-9]'), '')) ?? 0 : 0,
|
||||
'total_price':
|
||||
rentalData['totalPrice'] != null
|
||||
? int.tryParse(
|
||||
rentalData['totalPrice'].toString().replaceAll(
|
||||
RegExp(r'[^0-9]'),
|
||||
'',
|
||||
),
|
||||
) ??
|
||||
0
|
||||
: 0,
|
||||
'status': rentalData['status'] ?? 'MENUNGGU PEMBAYARAN',
|
||||
'created_at': DateTime.now().toString(),
|
||||
'denda': 0, // Default value
|
||||
@ -276,7 +292,7 @@ class PembayaranSewaController extends GetxController
|
||||
checkSewaAsetTableStructure();
|
||||
loadTagihanSewaDetails().then((_) {
|
||||
// Load existing payment proof images after tagihan_sewa details are loaded
|
||||
loadExistingPaymentProofImages();
|
||||
loadExistingPaymentProofImages(jenisPembayaran: 'tagihan awal');
|
||||
});
|
||||
loadSewaAsetDetails();
|
||||
loadBankAccounts(); // Load bank accounts data
|
||||
@ -286,7 +302,7 @@ class PembayaranSewaController extends GetxController
|
||||
loadOrderDetails();
|
||||
loadTagihanSewaDetails().then((_) {
|
||||
// Load existing payment proof images after tagihan_sewa details are loaded
|
||||
loadExistingPaymentProofImages();
|
||||
loadExistingPaymentProofImages(jenisPembayaran: 'tagihan awal');
|
||||
});
|
||||
loadSewaAsetDetails();
|
||||
loadBankAccounts(); // Load bank accounts data
|
||||
@ -382,18 +398,19 @@ class PembayaranSewaController extends GetxController
|
||||
}
|
||||
val?['quantity'] = data['kuantitas'] ?? 1;
|
||||
val?['denda'] =
|
||||
data['denda'] ??
|
||||
0; // Use data from API or default to 0
|
||||
val?['keterangan'] =
|
||||
data['keterangan'] ??
|
||||
''; // Use data from API or default to empty string
|
||||
|
||||
// Update status if it exists in the data
|
||||
if (data['status'] != null && data['status'].toString().isNotEmpty) {
|
||||
data['denda'] ?? 0; // Use data from API or default to 0
|
||||
val?['keterangan'] = data['keterangan'] ?? '';
|
||||
if (data['status'] != null &&
|
||||
data['status'].toString().isNotEmpty) {
|
||||
val?['status'] = data['status'];
|
||||
debugPrint('📊 Order status from sewa_aset: ${data['status']}');
|
||||
debugPrint(
|
||||
'📊 Order status from sewa_aset: \\${data['status']}',
|
||||
);
|
||||
}
|
||||
// Tambahkan mapping updated_at
|
||||
if (data['updated_at'] != null) {
|
||||
val?['updated_at'] = data['updated_at'];
|
||||
}
|
||||
|
||||
// Format rental period
|
||||
if (data['waktu_mulai'] != null &&
|
||||
data['waktu_selesai'] != null) {
|
||||
@ -401,12 +418,12 @@ class PembayaranSewaController extends GetxController
|
||||
final startTime = DateTime.parse(data['waktu_mulai']);
|
||||
final endTime = DateTime.parse(data['waktu_selesai']);
|
||||
val?['rental_period'] =
|
||||
'${startTime.day}/${startTime.month}/${startTime.year}, ${startTime.hour}:${startTime.minute.toString().padLeft(2, '0')} - ${endTime.hour}:${endTime.minute.toString().padLeft(2, '0')}';
|
||||
'\\${startTime.day}/\\${startTime.month}/\\${startTime.year}, \\${startTime.hour}:\\${startTime.minute.toString().padLeft(2, '0')} - \\${endTime.hour}:\\${endTime.minute.toString().padLeft(2, '0')}';
|
||||
debugPrint(
|
||||
'✅ Successfully formatted rental period: ${val?['rental_period']}',
|
||||
'✅ Successfully formatted rental period: \\${val?['rental_period']}',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error parsing date: $e');
|
||||
debugPrint('❌ Error parsing date: \\${e}');
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
@ -547,6 +564,11 @@ class PembayaranSewaController extends GetxController
|
||||
// Select payment type (tagihan_awal or denda)
|
||||
void selectPaymentType(String type) {
|
||||
selectedPaymentType.value = type;
|
||||
if (type == 'tagihan_awal') {
|
||||
loadExistingPaymentProofImages(jenisPembayaran: 'tagihan awal');
|
||||
} else if (type == 'denda') {
|
||||
loadExistingPaymentProofImages(jenisPembayaran: 'denda');
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
@ -558,21 +580,20 @@ class PembayaranSewaController extends GetxController
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 80,
|
||||
);
|
||||
|
||||
if (image != null) {
|
||||
// Add to the list of images instead of replacing
|
||||
paymentProofImages.add(File(image.path));
|
||||
|
||||
// Check for changes
|
||||
if (selectedPaymentType.value == 'denda') {
|
||||
paymentProofImagesDenda.add(File(image.path));
|
||||
} else {
|
||||
paymentProofImagesTagihanAwal.add(File(image.path));
|
||||
}
|
||||
_checkForChanges();
|
||||
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error taking photo: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal mengambil foto: ${e.toString()}',
|
||||
'Gagal mengambil foto: \\${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
@ -588,17 +609,20 @@ class PembayaranSewaController extends GetxController
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 80,
|
||||
);
|
||||
|
||||
if (image != null) {
|
||||
// Add to the list of images instead of replacing
|
||||
paymentProofImages.add(File(image.path));
|
||||
if (selectedPaymentType.value == 'denda') {
|
||||
paymentProofImagesDenda.add(File(image.path));
|
||||
} else {
|
||||
paymentProofImagesTagihanAwal.add(File(image.path));
|
||||
}
|
||||
_checkForChanges();
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error selecting photo from gallery: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memilih foto dari galeri: ${e.toString()}',
|
||||
'Gagal memilih foto dari galeri: \\${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
@ -607,7 +631,19 @@ class PembayaranSewaController extends GetxController
|
||||
}
|
||||
|
||||
// Upload payment proof to Supabase storage and save to foto_pembayaran table
|
||||
Future<void> uploadPaymentProof() async {
|
||||
Future<void> uploadPaymentProof({required String jenisPembayaran}) async {
|
||||
final paymentProofImages =
|
||||
jenisPembayaran == 'tagihan awal'
|
||||
? paymentProofImagesTagihanAwal
|
||||
: paymentProofImagesDenda;
|
||||
final imagesToDelete =
|
||||
jenisPembayaran == 'tagihan awal'
|
||||
? imagesToDeleteTagihanAwal
|
||||
: imagesToDeleteDenda;
|
||||
final hasUnsavedChanges =
|
||||
jenisPembayaran == 'tagihan awal'
|
||||
? hasUnsavedChangesTagihanAwal
|
||||
: hasUnsavedChangesDenda;
|
||||
// If there are no images and none marked for deletion, show error
|
||||
if (paymentProofImages.isEmpty && imagesToDelete.isEmpty) {
|
||||
Get.snackbar(
|
||||
@ -644,7 +680,9 @@ class PembayaranSewaController extends GetxController
|
||||
|
||||
// First, delete any images marked for deletion
|
||||
if (imagesToDelete.isNotEmpty) {
|
||||
debugPrint('🗑️ Deleting ${imagesToDelete.length} images from database and storage');
|
||||
debugPrint(
|
||||
'🗑️ Deleting ${imagesToDelete.length} images from database and storage',
|
||||
);
|
||||
|
||||
for (WebImageFile image in imagesToDelete) {
|
||||
// Delete the record from the foto_pembayaran table
|
||||
@ -673,14 +711,16 @@ class PembayaranSewaController extends GetxController
|
||||
// The filename is the last part of the path after the last '/'
|
||||
final String fileName = path.substring(path.lastIndexOf('/') + 1);
|
||||
|
||||
debugPrint('🗑️ Attempting to delete file from storage: $fileName');
|
||||
debugPrint(
|
||||
'🗑️ Attempting to delete file from storage: $fileName',
|
||||
);
|
||||
|
||||
// Delete the file from storage
|
||||
await client.storage
|
||||
.from('bukti.pembayaran')
|
||||
.remove([fileName]);
|
||||
await client.storage.from('bukti.pembayaran').remove([fileName]);
|
||||
|
||||
debugPrint('🗑️ Successfully deleted file from storage: $fileName');
|
||||
debugPrint(
|
||||
'🗑️ Successfully deleted file from storage: $fileName',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Error deleting file from storage: $e');
|
||||
// Continue even if file deletion fails - we've at least deleted from the database
|
||||
@ -693,7 +733,9 @@ class PembayaranSewaController extends GetxController
|
||||
}
|
||||
|
||||
// Upload each new image to Supabase Storage and save to database
|
||||
debugPrint('🔄 Uploading new payment proof images to Supabase storage...');
|
||||
debugPrint(
|
||||
'🔄 Uploading new payment proof images to Supabase storage...',
|
||||
);
|
||||
|
||||
List<String> uploadedUrls = [];
|
||||
List<dynamic> newImagesToUpload = [];
|
||||
@ -710,7 +752,9 @@ class PembayaranSewaController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('🔄 Found ${existingImageUrls.length} existing images and ${newImagesToUpload.length} new images to upload');
|
||||
debugPrint(
|
||||
'🔄 Found ${existingImageUrls.length} existing images and ${newImagesToUpload.length} new images to upload',
|
||||
);
|
||||
|
||||
// If there are new images to upload
|
||||
if (newImagesToUpload.isNotEmpty) {
|
||||
@ -721,13 +765,16 @@ class PembayaranSewaController extends GetxController
|
||||
// Upload each new image
|
||||
for (int i = 0; i < newImagesToUpload.length; i++) {
|
||||
final dynamic imageFile = newImagesToUpload[i];
|
||||
final String fileName = '${DateTime.now().millisecondsSinceEpoch}_${orderId.value}_$i.jpg';
|
||||
final String fileName =
|
||||
'${DateTime.now().millisecondsSinceEpoch}_${orderId.value}_$i.jpg';
|
||||
|
||||
// Create a sub-progress tracker for this image
|
||||
final subProgressNotifier = StreamController<double>();
|
||||
subProgressNotifier.stream.listen((subProgress) {
|
||||
// Calculate overall progress
|
||||
progressNotifier.add(currentProgress + (subProgress * progressIncrement));
|
||||
progressNotifier.add(
|
||||
currentProgress + (subProgress * progressIncrement),
|
||||
);
|
||||
});
|
||||
|
||||
// Upload to Supabase Storage
|
||||
@ -754,15 +801,20 @@ class PembayaranSewaController extends GetxController
|
||||
|
||||
// Save all new URLs to foto_pembayaran table
|
||||
for (String imageUrl in uploadedUrls) {
|
||||
await _saveToFotoPembayaranTable(imageUrl);
|
||||
await _saveToFotoPembayaranTable(imageUrl, jenisPembayaran);
|
||||
}
|
||||
|
||||
// Reload the existing images to get fresh data with new IDs
|
||||
await loadExistingPaymentProofImages();
|
||||
await loadExistingPaymentProofImages(jenisPembayaran: jenisPembayaran);
|
||||
|
||||
// Update order status in orderDetails
|
||||
orderDetails.update((val) {
|
||||
val?['status'] = 'MEMERIKSA PEMBAYARAN';
|
||||
if (jenisPembayaran == 'denda' &&
|
||||
val?['status'] == 'PEMBAYARAN DENDA') {
|
||||
val?['status'] = 'PERIKSA PEMBAYARAN DENDA';
|
||||
} else {
|
||||
val?['status'] = 'MEMERIKSA PEMBAYARAN';
|
||||
}
|
||||
});
|
||||
|
||||
// Also update the status in the sewa_aset table
|
||||
@ -771,17 +823,28 @@ class PembayaranSewaController extends GetxController
|
||||
final dynamic sewaAsetId = tagihanSewa.value['sewa_aset_id'];
|
||||
|
||||
if (sewaAsetId != null && sewaAsetId.toString().isNotEmpty) {
|
||||
debugPrint('🔄 Updating status in sewa_aset table for ID: $sewaAsetId');
|
||||
debugPrint(
|
||||
'🔄 Updating status in sewa_aset table for ID: $sewaAsetId',
|
||||
);
|
||||
|
||||
// Update the status in the sewa_aset table
|
||||
final updateResult = await client
|
||||
.from('sewa_aset')
|
||||
.update({'status': 'PERIKSA PEMBAYARAN'})
|
||||
.update({
|
||||
'status':
|
||||
(jenisPembayaran == 'denda' &&
|
||||
orderDetails.value['status'] ==
|
||||
'PERIKSA PEMBAYARAN DENDA')
|
||||
? 'PERIKSA PEMBAYARAN DENDA'
|
||||
: 'PERIKSA PEMBAYARAN',
|
||||
})
|
||||
.eq('id', sewaAsetId.toString());
|
||||
|
||||
debugPrint('✅ Status updated in sewa_aset table: $updateResult');
|
||||
} else {
|
||||
debugPrint('⚠️ Could not update sewa_aset status: No valid sewa_aset_id found');
|
||||
debugPrint(
|
||||
'⚠️ Could not update sewa_aset status: No valid sewa_aset_id found',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Don't fail the entire operation if this update fails
|
||||
@ -878,21 +941,23 @@ class PembayaranSewaController extends GetxController
|
||||
case 'DITERIMA':
|
||||
currentStep.value = 2;
|
||||
break;
|
||||
case 'PENGEMBALIAN':
|
||||
case 'AKTIF':
|
||||
currentStep.value = 3;
|
||||
break;
|
||||
case 'PEMBAYARAN DENDA':
|
||||
case 'PENGEMBALIAN':
|
||||
currentStep.value = 4;
|
||||
break;
|
||||
case 'MEMERIKSA PEMBAYARAN DENDA':
|
||||
case 'PEMBAYARAN DENDA':
|
||||
currentStep.value = 5;
|
||||
break;
|
||||
case 'SELESAI':
|
||||
case 'PERIKSA PEMBAYARAN DENDA':
|
||||
currentStep.value = 6;
|
||||
break;
|
||||
case 'SELESAI':
|
||||
currentStep.value = 7;
|
||||
break;
|
||||
case 'DIBATALKAN':
|
||||
// Special case for canceled orders
|
||||
currentStep.value = 0;
|
||||
currentStep.value = 8;
|
||||
break;
|
||||
default:
|
||||
currentStep.value = 0;
|
||||
@ -950,7 +1015,9 @@ class PembayaranSewaController extends GetxController
|
||||
debugPrint('Available fields in sewa_aset table:');
|
||||
|
||||
record.forEach((key, value) {
|
||||
debugPrint(' $key: (${value != null ? value.runtimeType : 'null'})');
|
||||
debugPrint(
|
||||
' $key: (${value != null ? value.runtimeType : 'null'})',
|
||||
);
|
||||
});
|
||||
|
||||
// Specifically check for time fields
|
||||
@ -987,12 +1054,16 @@ class PembayaranSewaController extends GetxController
|
||||
final data = await asetProvider.getBankAccounts();
|
||||
if (data.isNotEmpty) {
|
||||
bankAccounts.assignAll(data);
|
||||
debugPrint('✅ Bank accounts loaded: ${bankAccounts.length} accounts found');
|
||||
debugPrint(
|
||||
'✅ Bank accounts loaded: ${bankAccounts.length} accounts found',
|
||||
);
|
||||
|
||||
// Debug the bank accounts data
|
||||
debugPrint('📋 BANK ACCOUNTS DETAILS:');
|
||||
for (var account in bankAccounts) {
|
||||
debugPrint(' Bank: ${account['nama_bank']}, Account: ${account['nama_akun']}, Number: ${account['no_rekening']}');
|
||||
debugPrint(
|
||||
' Bank: ${account['nama_bank']}, Account: ${account['nama_akun']}, Number: ${account['no_rekening']}',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debugPrint('⚠️ No bank accounts found in akun_bank table');
|
||||
@ -1003,7 +1074,11 @@ class PembayaranSewaController extends GetxController
|
||||
}
|
||||
|
||||
// Helper method to upload image to Supabase storage
|
||||
Future<String?> _uploadToSupabaseStorage(dynamic imageFile, String fileName, StreamController<double> progressNotifier) async {
|
||||
Future<String?> _uploadToSupabaseStorage(
|
||||
dynamic imageFile,
|
||||
String fileName,
|
||||
StreamController<double> progressNotifier,
|
||||
) async {
|
||||
try {
|
||||
debugPrint('🔄 Uploading image to Supabase storage: $fileName');
|
||||
|
||||
@ -1031,7 +1106,9 @@ class PembayaranSewaController extends GetxController
|
||||
);
|
||||
|
||||
// Get public URL
|
||||
final String publicUrl = client.storage.from('bukti.pembayaran').getPublicUrl(fileName);
|
||||
final String publicUrl = client.storage
|
||||
.from('bukti.pembayaran')
|
||||
.getPublicUrl(fileName);
|
||||
|
||||
debugPrint('✅ Upload successful: $publicUrl');
|
||||
progressNotifier.add(1.0); // Upload complete
|
||||
@ -1050,7 +1127,10 @@ class PembayaranSewaController extends GetxController
|
||||
}
|
||||
|
||||
// Helper method to save image URL to foto_pembayaran table
|
||||
Future<void> _saveToFotoPembayaranTable(String imageUrl) async {
|
||||
Future<void> _saveToFotoPembayaranTable(
|
||||
String imageUrl,
|
||||
String jenisPembayaran,
|
||||
) async {
|
||||
try {
|
||||
debugPrint('🔄 Saving image URL to foto_pembayaran table...');
|
||||
|
||||
@ -1067,79 +1147,57 @@ class PembayaranSewaController extends GetxController
|
||||
final Map<String, dynamic> data = {
|
||||
'tagihan_sewa_id': tagihanSewaId,
|
||||
'foto_pembayaran': imageUrl,
|
||||
'jenis_pembayaran': jenisPembayaran,
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
};
|
||||
|
||||
// Insert data into the foto_pembayaran table
|
||||
final response = await client
|
||||
.from('foto_pembayaran')
|
||||
.insert(data)
|
||||
.select()
|
||||
.single();
|
||||
final response =
|
||||
await client.from('foto_pembayaran').insert(data).select().single();
|
||||
|
||||
debugPrint('✅ Image URL saved to foto_pembayaran table: ${response['id']}');
|
||||
debugPrint(
|
||||
'✅ Image URL saved to foto_pembayaran table: ${response['id']}',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error in _saveToFotoPembayaranTable: $e');
|
||||
throw Exception('Failed to save image URL to database: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Load existing payment proof images
|
||||
Future<void> loadExistingPaymentProofImages() async {
|
||||
// Load existing payment proof images for a specific jenis_pembayaran
|
||||
Future<void> loadExistingPaymentProofImages({
|
||||
required String jenisPembayaran,
|
||||
}) async {
|
||||
try {
|
||||
debugPrint('🔄 Loading existing payment proof images for tagihan_sewa_id: ${tagihanSewa.value['id']}');
|
||||
|
||||
// Check if we have a valid tagihan_sewa_id
|
||||
debugPrint(
|
||||
'🔄 Loading existing payment proof images for tagihan_sewa_id: \\${tagihanSewa.value['id']} dan jenis_pembayaran: $jenisPembayaran',
|
||||
);
|
||||
final dynamic tagihanSewaId = tagihanSewa.value['id'];
|
||||
if (tagihanSewaId == null || tagihanSewaId.toString().isEmpty) {
|
||||
debugPrint('⚠️ No valid tagihan_sewa_id found, skipping image load');
|
||||
return;
|
||||
}
|
||||
|
||||
// First, make a test query to see the structure of the response
|
||||
final testResponse = await client
|
||||
.from('foto_pembayaran')
|
||||
.select()
|
||||
.limit(1);
|
||||
|
||||
// Log the test response structure
|
||||
if (testResponse.isNotEmpty) {
|
||||
debugPrint('💾 DEBUG: Test database response: ${testResponse[0]}');
|
||||
testResponse[0].forEach((key, value) {
|
||||
debugPrint('💾 DEBUG: Field $key = $value (${value?.runtimeType})');
|
||||
});
|
||||
}
|
||||
|
||||
// Now make the actual query for this tagihan_sewa_id
|
||||
final List<dynamic> response = await client
|
||||
.from('foto_pembayaran')
|
||||
.select()
|
||||
.eq('tagihan_sewa_id', tagihanSewaId)
|
||||
.eq('jenis_pembayaran', jenisPembayaran)
|
||||
.order('created_at', ascending: false);
|
||||
|
||||
debugPrint('🔄 Found ${response.length} existing payment proof images');
|
||||
|
||||
// Clear existing tracking lists
|
||||
paymentProofImages.clear();
|
||||
originalImages.clear();
|
||||
imagesToDelete.clear();
|
||||
hasUnsavedChanges.value = false;
|
||||
|
||||
// Process each image in the response
|
||||
debugPrint(
|
||||
'🔄 Found \\${response.length} existing payment proof images for $jenisPembayaran',
|
||||
);
|
||||
final targetList =
|
||||
jenisPembayaran == 'tagihan awal'
|
||||
? paymentProofImagesTagihanAwal
|
||||
: paymentProofImagesDenda;
|
||||
targetList.clear();
|
||||
for (final item in response) {
|
||||
// Extract the image URL
|
||||
final String imageUrl = item['foto_pembayaran'];
|
||||
|
||||
// Extract the ID - debug the item structure
|
||||
debugPrint('💾 Image data: $item');
|
||||
|
||||
// Get the ID field - in Supabase, this is a UUID string
|
||||
String imageId = '';
|
||||
try {
|
||||
if (item.containsKey('id')) {
|
||||
final dynamic rawId = item['id'];
|
||||
if (rawId != null) {
|
||||
// Store ID as string since it's a UUID
|
||||
imageId = rawId.toString();
|
||||
}
|
||||
debugPrint('🔄 Image ID: $imageId');
|
||||
@ -1147,21 +1205,12 @@ class PembayaranSewaController extends GetxController
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error getting image ID: $e');
|
||||
}
|
||||
|
||||
// Create the WebImageFile object
|
||||
final webImageFile = WebImageFile(imageUrl);
|
||||
webImageFile.id = imageId;
|
||||
|
||||
// Add to tracking lists
|
||||
paymentProofImages.add(webImageFile);
|
||||
originalImages.add(webImageFile);
|
||||
|
||||
targetList.add(webImageFile);
|
||||
debugPrint('✅ Added image: $imageUrl with ID: $imageId');
|
||||
}
|
||||
|
||||
// Update the UI
|
||||
update();
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error loading payment proof images: $e');
|
||||
}
|
||||
@ -1174,7 +1223,9 @@ class PembayaranSewaController extends GetxController
|
||||
|
||||
try {
|
||||
// Reload all data
|
||||
await Future.delayed(const Duration(milliseconds: 500)); // Small delay for better UX
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
); // Small delay for better UX
|
||||
loadOrderDetails();
|
||||
loadTagihanSewaDetails();
|
||||
loadSewaAsetDetails();
|
||||
|
@ -88,6 +88,19 @@ class SewaAsetController extends GetxController
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
debugPrint('🚀 SewaAsetController: onReady called');
|
||||
// Set tab index from arguments (if any) after build
|
||||
Future.delayed(Duration.zero, () {
|
||||
final args = Get.arguments;
|
||||
if (args != null && args is Map && args['tab'] != null) {
|
||||
int initialTab =
|
||||
args['tab'] is int
|
||||
? args['tab']
|
||||
: int.tryParse(args['tab'].toString()) ?? 0;
|
||||
if (tabController.length > initialTab) {
|
||||
tabController.index = initialTab;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -2,11 +2,14 @@ import 'package:get/get.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import '../../../services/navigation_service.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class WargaDashboardController extends GetxController {
|
||||
// Dependency injection
|
||||
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
||||
final NavigationService navigationService = Get.find<NavigationService>();
|
||||
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
||||
|
||||
// User data
|
||||
final userName = 'Pengguna Warga'.obs;
|
||||
@ -28,6 +31,11 @@ class WargaDashboardController extends GetxController {
|
||||
// Active penalties
|
||||
final activePenalties = <Map<String, dynamic>>[].obs;
|
||||
|
||||
// Summary counts
|
||||
final diterimaCount = 0.obs;
|
||||
final tagihanAktifCount = 0.obs;
|
||||
final dendaAktifCount = 0.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
@ -36,6 +44,7 @@ class WargaDashboardController extends GetxController {
|
||||
navigationService.setNavIndex(0);
|
||||
|
||||
// Load user data
|
||||
fetchProfileFromWargaDesa();
|
||||
_loadUserData();
|
||||
|
||||
// Load sample data
|
||||
@ -46,6 +55,12 @@ class WargaDashboardController extends GetxController {
|
||||
|
||||
// Load unpaid rentals
|
||||
loadUnpaidRentals();
|
||||
|
||||
// Debug count sewa_aset by status
|
||||
_debugCountSewaAset();
|
||||
|
||||
// Load sewa aktif
|
||||
loadActiveRentals();
|
||||
}
|
||||
|
||||
Future<void> _loadUserData() async {
|
||||
@ -112,7 +127,7 @@ class WargaDashboardController extends GetxController {
|
||||
}
|
||||
|
||||
void refreshData() {
|
||||
// Refresh data from repository
|
||||
fetchProfileFromWargaDesa();
|
||||
_loadSampleData();
|
||||
loadDummyData();
|
||||
}
|
||||
@ -129,12 +144,17 @@ class WargaDashboardController extends GetxController {
|
||||
// Already on Home tab
|
||||
break;
|
||||
case 1:
|
||||
// Navigate to Sewa page
|
||||
navigationService.toWargaSewa();
|
||||
// Navigate to Sewa page, tab Aktif
|
||||
toWargaSewaTabAktif();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void toWargaSewaTabAktif() {
|
||||
// Navigasi ke halaman warga sewa dan tab Aktif (index 3)
|
||||
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 3});
|
||||
}
|
||||
|
||||
void logout() async {
|
||||
await _authProvider.signOut();
|
||||
navigationService.toLogin();
|
||||
@ -177,4 +197,137 @@ class WargaDashboardController extends GetxController {
|
||||
print('Error loading unpaid rentals: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _debugCountSewaAset() async {
|
||||
diterimaCount.value = await _asetProvider.countSewaAsetByStatus([
|
||||
'DITERIMA',
|
||||
]);
|
||||
tagihanAktifCount.value = await _asetProvider.countSewaAsetByStatus([
|
||||
'MENUNGGU PEMBAYARAN',
|
||||
'PERIKSA PEMBAYARAN',
|
||||
]);
|
||||
dendaAktifCount.value = await _asetProvider.countSewaAsetByStatus([
|
||||
'PEMBAYARAN DENDA',
|
||||
'PERIKSA PEMBAYARAN DENDA',
|
||||
]);
|
||||
print('[DEBUG] Jumlah sewa diterima: ${diterimaCount.value}');
|
||||
print('[DEBUG] Jumlah tagihan aktif: ${tagihanAktifCount.value}');
|
||||
print('[DEBUG] Jumlah denda aktif: ${dendaAktifCount.value}');
|
||||
}
|
||||
|
||||
Future<void> loadActiveRentals() async {
|
||||
try {
|
||||
activeRentals.clear();
|
||||
final sewaAsetList = await _authProvider.getSewaAsetByStatus(['AKTIF']);
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await _asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
if (sewaAset['waktu_mulai'] != null &&
|
||||
sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
String duration = '-';
|
||||
final tagihan = await _asetProvider.getTagihanSewa(sewaAset['id']);
|
||||
if (tagihan != null) {
|
||||
final durasiTagihan = tagihan['durasi'] ?? sewaAset['durasi'];
|
||||
final satuanTagihan = tagihan['nama_satuan_waktu'] ?? namaSatuanWaktu;
|
||||
duration = '${durasiTagihan ?? '-'} ${satuanTagihan ?? ''}';
|
||||
} else {
|
||||
duration = '${sewaAset['durasi'] ?? '-'} ${namaSatuanWaktu ?? ''}';
|
||||
}
|
||||
activeRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': duration,
|
||||
'status': sewaAset['status'] ?? 'AKTIF',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
'can_extend': sewaAset['can_extend'] == true,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading active rentals: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void toSewaAsetTabPaket() {
|
||||
// Navigasi ke halaman sewa_aset tab Paket (index 1)
|
||||
Get.toNamed(Routes.SEWA_ASET, arguments: {'tab': 1});
|
||||
}
|
||||
|
||||
Future<void> fetchProfileFromWargaDesa() async {
|
||||
try {
|
||||
final user = _authProvider.currentUser;
|
||||
if (user == null) 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,8 @@ class WargaSewaController extends GetxController
|
||||
final acceptedRentals = <Map<String, dynamic>>[].obs;
|
||||
final completedRentals = <Map<String, dynamic>>[].obs;
|
||||
final cancelledRentals = <Map<String, dynamic>>[].obs;
|
||||
final returnedRentals = <Map<String, dynamic>>[].obs;
|
||||
final activeRentals = <Map<String, dynamic>>[].obs;
|
||||
|
||||
// Loading states
|
||||
final isLoading = false.obs;
|
||||
@ -32,26 +34,26 @@ class WargaSewaController extends GetxController
|
||||
final isLoadingAccepted = false.obs;
|
||||
final isLoadingCompleted = false.obs;
|
||||
final isLoadingCancelled = false.obs;
|
||||
final isLoadingReturned = false.obs;
|
||||
final isLoadingActive = false.obs;
|
||||
|
||||
bool _tabSetFromArgument = false;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// Ensure tab index is set to Sewa (1)
|
||||
navigationService.setNavIndex(1);
|
||||
|
||||
// Initialize tab controller with 6 tabs
|
||||
tabController = TabController(length: 6, vsync: this);
|
||||
|
||||
// Set initial tab and ensure tab view is updated
|
||||
tabController.index = 0;
|
||||
// Initialize tab controller with 7 tabs
|
||||
tabController = TabController(length: 7, vsync: this);
|
||||
|
||||
// Load real rental data for all tabs
|
||||
loadRentalsData();
|
||||
loadPendingRentals();
|
||||
loadAcceptedRentals();
|
||||
loadActiveRentals();
|
||||
loadCompletedRentals();
|
||||
loadCancelledRentals();
|
||||
loadReturnedRentals();
|
||||
|
||||
// Listen to tab changes to update state if needed
|
||||
tabController.addListener(() {
|
||||
@ -77,7 +79,9 @@ class WargaSewaController extends GetxController
|
||||
}
|
||||
break;
|
||||
case 3: // Aktif
|
||||
// Add Aktif tab logic when needed
|
||||
if (activeRentals.isEmpty && !isLoadingActive.value) {
|
||||
loadActiveRentals();
|
||||
}
|
||||
break;
|
||||
case 4: // Selesai
|
||||
if (completedRentals.isEmpty && !isLoadingCompleted.value) {
|
||||
@ -89,6 +93,11 @@ class WargaSewaController extends GetxController
|
||||
loadCancelledRentals();
|
||||
}
|
||||
break;
|
||||
case 6: // Dikembalikan
|
||||
if (returnedRentals.isEmpty && !isLoadingReturned.value) {
|
||||
loadReturnedRentals();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -96,9 +105,26 @@ class WargaSewaController extends GetxController
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
// Ensure nav index is set to Sewa (1) when the controller is ready
|
||||
// This helps maintain correct state during hot reload
|
||||
navigationService.setNavIndex(1);
|
||||
// Jalankan update nav index dan tab index setelah build selesai
|
||||
Future.delayed(Duration.zero, () {
|
||||
navigationService.setNavIndex(1);
|
||||
|
||||
final args = Get.arguments;
|
||||
int initialTab = 0;
|
||||
if (!_tabSetFromArgument &&
|
||||
args != null &&
|
||||
args is Map &&
|
||||
args['tab'] != null) {
|
||||
initialTab =
|
||||
args['tab'] is int
|
||||
? args['tab']
|
||||
: int.tryParse(args['tab'].toString()) ?? 0;
|
||||
if (tabController.length > initialTab) {
|
||||
tabController.index = initialTab;
|
||||
_tabSetFromArgument = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -118,7 +144,7 @@ class WargaSewaController extends GetxController
|
||||
// Get sewa_aset data with status "MENUNGGU PEMBAYARAN" or "PEMBAYARAN DENDA"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||
'MENUNGGU PEMBAYARAN',
|
||||
'PEMBAYARAN DENDA'
|
||||
'PEMBAYARAN DENDA',
|
||||
]);
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} sewa_aset records');
|
||||
@ -147,7 +173,8 @@ class WargaSewaController extends GetxController
|
||||
String jamSelesai = '';
|
||||
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']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
@ -175,8 +202,9 @@ class WargaSewaController extends GetxController
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
@ -208,6 +236,7 @@ class WargaSewaController extends GetxController
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
'updated_at': sewaAset['updated_at'],
|
||||
});
|
||||
}
|
||||
|
||||
@ -245,12 +274,54 @@ class WargaSewaController extends GetxController
|
||||
}
|
||||
|
||||
// Actions
|
||||
void cancelRental(String id) {
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Pembatalan berhasil',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
void cancelRental(String id) async {
|
||||
final confirmed = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: const Text('Konfirmasi Pembatalan'),
|
||||
content: const Text('Apakah Anda yakin ingin membatalkan pesanan ini?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Tidak'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('Ya, Batalkan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
await asetProvider.client
|
||||
.from('sewa_aset')
|
||||
.update({'status': 'DIBATALKAN'})
|
||||
.eq('id', id);
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Pesanan berhasil dibatalkan',
|
||||
snackPosition: SnackPosition.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
|
||||
@ -260,10 +331,7 @@ class WargaSewaController extends GetxController
|
||||
// Navigate to payment page with rental data
|
||||
Get.toNamed(
|
||||
Routes.PEMBAYARAN_SEWA,
|
||||
arguments: {
|
||||
'orderId': rental['id'],
|
||||
'rentalData': rental,
|
||||
},
|
||||
arguments: {'orderId': rental['id'], 'rentalData': rental},
|
||||
);
|
||||
}
|
||||
|
||||
@ -312,7 +380,8 @@ class WargaSewaController extends GetxController
|
||||
String jamSelesai = '';
|
||||
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']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
@ -340,8 +409,9 @@ class WargaSewaController extends GetxController
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
@ -375,7 +445,9 @@ class WargaSewaController extends GetxController
|
||||
});
|
||||
}
|
||||
|
||||
debugPrint('Processed ${completedRentals.length} completed rental records');
|
||||
debugPrint(
|
||||
'Processed ${completedRentals.length} completed rental records',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error loading completed rentals data: $e');
|
||||
} finally {
|
||||
@ -392,7 +464,9 @@ class WargaSewaController extends GetxController
|
||||
cancelledRentals.clear();
|
||||
|
||||
// Get sewa_aset data with status "DIBATALKAN"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['DIBATALKAN']);
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||
'DIBATALKAN',
|
||||
]);
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} cancelled sewa_aset records');
|
||||
|
||||
@ -420,7 +494,8 @@ class WargaSewaController extends GetxController
|
||||
String jamSelesai = '';
|
||||
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']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
@ -448,8 +523,9 @@ class WargaSewaController extends GetxController
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
@ -484,7 +560,9 @@ class WargaSewaController extends GetxController
|
||||
});
|
||||
}
|
||||
|
||||
debugPrint('Processed ${cancelledRentals.length} cancelled rental records');
|
||||
debugPrint(
|
||||
'Processed ${cancelledRentals.length} cancelled rental records',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error loading cancelled rentals data: $e');
|
||||
} finally {
|
||||
@ -500,8 +578,11 @@ class WargaSewaController extends GetxController
|
||||
// Clear existing data
|
||||
pendingRentals.clear();
|
||||
|
||||
// Get sewa_aset data with status "PERIKSA PEMBAYARAN"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['PERIKSA PEMBAYARAN']);
|
||||
// Get sewa_aset data with status 'PERIKSA PEMBAYARAN' dan 'PERIKSA PEMBAYARAN DENDA'
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||
'PERIKSA PEMBAYARAN',
|
||||
'PERIKSA PEMBAYARAN DENDA',
|
||||
]);
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} pending sewa_aset records');
|
||||
|
||||
@ -529,7 +610,8 @@ class WargaSewaController extends GetxController
|
||||
String jamSelesai = '';
|
||||
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']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
@ -557,8 +639,9 @@ class WargaSewaController extends GetxController
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
@ -637,7 +720,8 @@ class WargaSewaController extends GetxController
|
||||
String jamSelesai = '';
|
||||
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']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
@ -665,8 +749,9 @@ class WargaSewaController extends GetxController
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
@ -707,4 +792,166 @@ class WargaSewaController extends GetxController
|
||||
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
@ -117,6 +117,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
),
|
||||
],
|
||||
),
|
||||
dividerColor: Colors.transparent,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: const Color(
|
||||
0xFF718093,
|
||||
|
@ -154,10 +154,10 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
'route': () => controller.navigateToRentals(),
|
||||
},
|
||||
{
|
||||
'title': 'Bayar',
|
||||
'icon': Icons.payment_outlined,
|
||||
'title': 'Paket',
|
||||
'icon': Icons.widgets_outlined,
|
||||
'color': const Color(0xFF2196F3),
|
||||
'route': () => Get.toNamed(Routes.PEMBAYARAN_SEWA),
|
||||
'route': () => controller.toSewaAsetTabPaket(),
|
||||
},
|
||||
];
|
||||
|
||||
@ -218,32 +218,44 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
child: Column(
|
||||
children: [
|
||||
// Sewa Diterima
|
||||
_buildActivityCard(
|
||||
title: 'Sewa Diterima',
|
||||
value: controller.activeRentals.length.toString(),
|
||||
icon: Icons.check_circle_outline,
|
||||
color: AppColors.success,
|
||||
onTap: () => controller.navigateToRentals(),
|
||||
Obx(
|
||||
() => _buildActivityCard(
|
||||
title: 'Sewa Diterima',
|
||||
value: controller.diterimaCount.value.toString(),
|
||||
icon: Icons.check_circle_outline,
|
||||
color: AppColors.success,
|
||||
onTap:
|
||||
() =>
|
||||
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 2}),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Tagihan Aktif
|
||||
_buildActivityCard(
|
||||
title: 'Tagihan Aktif',
|
||||
value: controller.activeBills.length.toString(),
|
||||
icon: Icons.receipt_long_outlined,
|
||||
color: AppColors.warning,
|
||||
onTap: () => Get.toNamed(Routes.PEMBAYARAN_SEWA),
|
||||
Obx(
|
||||
() => _buildActivityCard(
|
||||
title: 'Tagihan Aktif',
|
||||
value: controller.tagihanAktifCount.value.toString(),
|
||||
icon: Icons.receipt_long_outlined,
|
||||
color: AppColors.warning,
|
||||
onTap:
|
||||
() =>
|
||||
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 0}),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Denda Aktif
|
||||
_buildActivityCard(
|
||||
title: 'Denda Aktif',
|
||||
value: controller.activePenalties.length.toString(),
|
||||
icon: Icons.warning_amber_outlined,
|
||||
color: AppColors.error,
|
||||
onTap: () => Get.toNamed(Routes.PEMBAYARAN_SEWA),
|
||||
Obx(
|
||||
() => _buildActivityCard(
|
||||
title: 'Denda Aktif',
|
||||
value: controller.dendaAktifCount.value.toString(),
|
||||
icon: Icons.warning_amber_outlined,
|
||||
color: AppColors.error,
|
||||
onTap:
|
||||
() =>
|
||||
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 0}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -357,7 +369,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Sewa Diterima',
|
||||
'Sewa Aktif',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -498,31 +510,34 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Asset icon
|
||||
// Asset icon/gambar
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
AppColors.primary.withOpacity(0.7),
|
||||
AppColors.primary,
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primary.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
color: AppColors.primary.withOpacity(0.08),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.local_shipping,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child:
|
||||
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),
|
||||
@ -533,7 +548,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
rental['name'],
|
||||
rental['name'] ?? '-',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -542,7 +557,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
rental['time'],
|
||||
rental['waktuSewa'] ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.textSecondary,
|
||||
@ -567,7 +582,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
rental['price'],
|
||||
rental['totalPrice'] ?? 'Rp 0',
|
||||
style: TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -591,14 +606,14 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
child: _buildInfoItem(
|
||||
icon: Icons.timer_outlined,
|
||||
title: 'Durasi',
|
||||
value: rental['duration'],
|
||||
value: rental['duration'] ?? '-',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildInfoItem(
|
||||
icon: Icons.calendar_today_outlined,
|
||||
title: 'Status',
|
||||
value: 'Diterima',
|
||||
value: rental['status'] ?? '-',
|
||||
valueColor: AppColors.success,
|
||||
),
|
||||
),
|
||||
@ -608,7 +623,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Action buttons
|
||||
if (rental['can_extend'])
|
||||
if ((rental['can_extend'] ?? false) == true)
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => controller.extendRental(rental['id']),
|
||||
icon: const Icon(Icons.update, size: 18),
|
||||
|
@ -3,12 +3,16 @@ import 'package:get/get.dart';
|
||||
import '../controllers/warga_dashboard_controller.dart';
|
||||
import '../views/warga_layout.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import '../../../widgets/app_drawer.dart';
|
||||
import '../../../services/navigation_service.dart';
|
||||
|
||||
class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
const WargaProfileView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final navigationService = Get.find<NavigationService>();
|
||||
navigationService.setNavIndex(2);
|
||||
return WargaLayout(
|
||||
appBar: AppBar(
|
||||
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,
|
||||
body: RefreshIndicator(
|
||||
color: AppColors.primary,
|
||||
|
File diff suppressed because it is too large
Load Diff
88
lib/app/services/pembayaran_service.dart
Normal file
88
lib/app/services/pembayaran_service.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@ import 'package:get/get.dart';
|
||||
import 'navigation_service.dart';
|
||||
import '../data/providers/auth_provider.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 ServiceManager {
|
||||
@ -26,6 +28,11 @@ abstract class ServiceManager {
|
||||
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
|
||||
// This ensures it's always available for the drawer
|
||||
registerWargaDashboardController();
|
||||
|
222
lib/app/services/sewa_service.dart
Normal file
222
lib/app/services/sewa_service.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'app_colors.dart';
|
||||
|
||||
/// App theme configuration
|
||||
@ -19,13 +20,16 @@ class AppTheme {
|
||||
),
|
||||
scaffoldBackgroundColor: AppColors.background,
|
||||
|
||||
// Set Lato as the default font for the entire app
|
||||
fontFamily: GoogleFonts.lato().fontFamily,
|
||||
|
||||
// App bar theme
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
iconTheme: const IconThemeData(color: Colors.white),
|
||||
titleTextStyle: const TextStyle(
|
||||
titleTextStyle: GoogleFonts.lato(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
@ -50,7 +54,10 @@ class AppTheme {
|
||||
shape: RoundedRectangleBorder(
|
||||
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(
|
||||
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(
|
||||
foregroundColor: AppColors.primary,
|
||||
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),
|
||||
borderSide: BorderSide(color: AppColors.error, width: 1.5),
|
||||
),
|
||||
hintStyle: TextStyle(color: AppColors.textLight),
|
||||
labelStyle: TextStyle(color: AppColors.textSecondary),
|
||||
hintStyle: GoogleFonts.lato(color: AppColors.textLight),
|
||||
labelStyle: GoogleFonts.lato(color: AppColors.textSecondary),
|
||||
),
|
||||
|
||||
// Checkbox theme
|
||||
@ -115,21 +128,21 @@ class AppTheme {
|
||||
|
||||
// Text themes
|
||||
textTheme: TextTheme(
|
||||
displayLarge: TextStyle(color: AppColors.textPrimary),
|
||||
displayMedium: TextStyle(color: AppColors.textPrimary),
|
||||
displaySmall: TextStyle(color: AppColors.textPrimary),
|
||||
headlineLarge: TextStyle(color: AppColors.textPrimary),
|
||||
headlineMedium: TextStyle(color: AppColors.textPrimary),
|
||||
headlineSmall: TextStyle(color: AppColors.textPrimary),
|
||||
titleLarge: TextStyle(color: AppColors.textPrimary),
|
||||
titleMedium: TextStyle(color: AppColors.textPrimary),
|
||||
titleSmall: TextStyle(color: AppColors.textPrimary),
|
||||
bodyLarge: TextStyle(color: AppColors.textPrimary),
|
||||
bodyMedium: TextStyle(color: AppColors.textPrimary),
|
||||
bodySmall: TextStyle(color: AppColors.textSecondary),
|
||||
labelLarge: TextStyle(color: AppColors.textPrimary),
|
||||
labelMedium: TextStyle(color: AppColors.textSecondary),
|
||||
labelSmall: TextStyle(color: AppColors.textLight),
|
||||
displayLarge: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||
displayMedium: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||
displaySmall: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||
headlineLarge: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||
headlineMedium: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||
headlineSmall: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||
titleLarge: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||
titleMedium: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||
titleSmall: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||
bodyLarge: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||
bodyMedium: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||
bodySmall: GoogleFonts.lato(color: AppColors.textSecondary),
|
||||
labelLarge: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||
labelMedium: GoogleFonts.lato(color: AppColors.textSecondary),
|
||||
labelSmall: GoogleFonts.lato(color: AppColors.textLight),
|
||||
),
|
||||
|
||||
// Divider theme
|
||||
|
10
lib/app/utils/format_utils.dart
Normal file
10
lib/app/utils/format_utils.dart
Normal 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);
|
||||
}
|
@ -291,34 +291,6 @@ class AppDrawer extends StatelessWidget {
|
||||
),
|
||||
|
||||
// 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(
|
||||
icon: Icons.logout_rounded,
|
||||
title: 'Keluar',
|
||||
|
@ -525,6 +525,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logger
|
||||
sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -48,6 +48,7 @@ dependencies:
|
||||
flutter_dotenv: ^5.1.0
|
||||
image_picker: ^1.0.7
|
||||
intl: 0.19.0
|
||||
logger: ^2.1.0
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
get_storage: ^2.1.1
|
||||
|
30
widget_test.dart
Normal file
30
widget_test.dart
Normal 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);
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user