fitur petugas
This commit is contained in:
@ -5,6 +5,7 @@ class AsetModel {
|
|||||||
final String nama;
|
final String nama;
|
||||||
final String deskripsi;
|
final String deskripsi;
|
||||||
final String kategori;
|
final String kategori;
|
||||||
|
final String jenis; // Add this line
|
||||||
final int harga;
|
final int harga;
|
||||||
final int? denda;
|
final int? denda;
|
||||||
final String status;
|
final String status;
|
||||||
@ -14,17 +15,21 @@ class AsetModel {
|
|||||||
final int? kuantitasTerpakai;
|
final int? kuantitasTerpakai;
|
||||||
final String? satuanUkur;
|
final String? satuanUkur;
|
||||||
|
|
||||||
// Untuk menampung URL gambar pertama dari tabel foto_aset
|
// URL gambar utama (untuk backward compatibility)
|
||||||
String? imageUrl;
|
String? imageUrl;
|
||||||
|
|
||||||
|
// List untuk menyimpan semua URL gambar aset
|
||||||
|
final RxList<String> imageUrls = <String>[].obs;
|
||||||
|
|
||||||
// Menggunakan RxList untuk membuatnya mutable dan reaktif
|
// Menggunakan RxList untuk membuatnya mutable dan reaktif
|
||||||
RxList<Map<String, dynamic>> satuanWaktuSewa = <Map<String, dynamic>>[].obs;
|
final RxList<Map<String, dynamic>> satuanWaktuSewa = <Map<String, dynamic>>[].obs;
|
||||||
|
|
||||||
AsetModel({
|
AsetModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.nama,
|
required this.nama,
|
||||||
required this.deskripsi,
|
required this.deskripsi,
|
||||||
required this.kategori,
|
required this.kategori,
|
||||||
|
this.jenis = 'Sewa', // Add this line with default value
|
||||||
required this.harga,
|
required this.harga,
|
||||||
this.denda,
|
this.denda,
|
||||||
required this.status,
|
required this.status,
|
||||||
@ -42,31 +47,69 @@ class AsetModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Menambahkan URL gambar dari JSON
|
||||||
|
void addImageUrl(String? url) {
|
||||||
|
if (url != null && url.isNotEmpty && !imageUrls.contains(url)) {
|
||||||
|
imageUrls.add(url);
|
||||||
|
// Update imageUrl untuk backward compatibility
|
||||||
|
if (imageUrl == null) {
|
||||||
|
imageUrl = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menghapus URL gambar
|
||||||
|
bool removeImageUrl(String url) {
|
||||||
|
final removed = imageUrls.remove(url);
|
||||||
|
if (removed && imageUrl == url) {
|
||||||
|
imageUrl = imageUrls.isNotEmpty ? imageUrls.first : null;
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
factory AsetModel.fromJson(Map<String, dynamic> json) {
|
factory AsetModel.fromJson(Map<String, dynamic> json) {
|
||||||
return AsetModel(
|
final model = AsetModel(
|
||||||
id: json['id'] ?? '',
|
id: json['id'] ?? '',
|
||||||
nama: json['nama'] ?? '',
|
nama: json['nama'] ?? '',
|
||||||
deskripsi: json['deskripsi'] ?? '',
|
deskripsi: json['deskripsi'] ?? '',
|
||||||
kategori: json['kategori'] ?? '',
|
kategori: json['kategori'] ?? '',
|
||||||
|
jenis: json['jenis'] ?? 'Sewa',
|
||||||
harga: json['harga'] ?? 0,
|
harga: json['harga'] ?? 0,
|
||||||
denda: json['denda'],
|
denda: json['denda'],
|
||||||
status: json['status'] ?? '',
|
status: json['status'] ?? '',
|
||||||
createdAt:
|
createdAt: json['created_at'] != null
|
||||||
json['created_at'] != null
|
|
||||||
? DateTime.parse(json['created_at'])
|
? DateTime.parse(json['created_at'])
|
||||||
: null,
|
: null,
|
||||||
updatedAt:
|
updatedAt: json['updated_at'] != null
|
||||||
json['updated_at'] != null
|
|
||||||
? DateTime.parse(json['updated_at'])
|
? DateTime.parse(json['updated_at'])
|
||||||
: null,
|
: null,
|
||||||
kuantitas: json['kuantitas'],
|
kuantitas: json['kuantitas'],
|
||||||
kuantitasTerpakai: json['kuantitas_terpakai'],
|
kuantitasTerpakai: json['kuantitas_terpakai'],
|
||||||
satuanUkur: json['satuan_ukur'],
|
satuanUkur: json['satuan_ukur'],
|
||||||
|
imageUrl: json['foto_aset'],
|
||||||
|
initialSatuanWaktuSewa: json['satuan_waktu_sewa'] != null
|
||||||
|
? List<Map<String, dynamic>>.from(json['satuan_waktu_sewa'])
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add the main image URL to the list if it exists
|
||||||
|
if (json['foto_aset'] != null) {
|
||||||
|
model.addImageUrl(json['foto_aset']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any additional image URLs if they exist in the JSON
|
||||||
|
if (json['foto_aset_tambahan'] != null) {
|
||||||
|
final additionalImages = List<String>.from(json['foto_aset_tambahan']);
|
||||||
|
for (final url in additionalImages) {
|
||||||
|
model.addImageUrl(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
final data = <String, dynamic>{
|
||||||
'id': id,
|
'id': id,
|
||||||
'nama': nama,
|
'nama': nama,
|
||||||
'deskripsi': deskripsi,
|
'deskripsi': deskripsi,
|
||||||
@ -80,5 +123,23 @@ class AsetModel {
|
|||||||
'kuantitas_terpakai': kuantitasTerpakai,
|
'kuantitas_terpakai': kuantitasTerpakai,
|
||||||
'satuan_ukur': satuanUkur,
|
'satuan_ukur': satuanUkur,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add image URLs if they exist
|
||||||
|
if (imageUrls.isNotEmpty) {
|
||||||
|
data['foto_aset'] = imageUrl;
|
||||||
|
|
||||||
|
// Add additional images (excluding the main image)
|
||||||
|
final additionalImages = imageUrls.where((url) => url != imageUrl).toList();
|
||||||
|
if (additionalImages.isNotEmpty) {
|
||||||
|
data['foto_aset_tambahan'] = additionalImages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add rental time units if they exist
|
||||||
|
if (satuanWaktuSewa.isNotEmpty) {
|
||||||
|
data['satuan_waktu_sewa'] = satuanWaktuSewa.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:developer' as developer;
|
||||||
|
|
||||||
class PaketModel {
|
class PaketModel {
|
||||||
final String id;
|
final String id;
|
||||||
@ -6,12 +7,13 @@ class PaketModel {
|
|||||||
final String deskripsi;
|
final String deskripsi;
|
||||||
final double harga;
|
final double harga;
|
||||||
final int kuantitas;
|
final int kuantitas;
|
||||||
final List<String> foto;
|
final String status;
|
||||||
final List<Map<String, dynamic>> satuanWaktuSewa;
|
List<String> foto;
|
||||||
|
List<Map<String, dynamic>> satuanWaktuSewa;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
final String? foto_paket; // Main photo URL
|
String? foto_paket; // Main photo URL
|
||||||
final List<String>? images; // List of photo URLs
|
List<String>? images; // List of photo URLs
|
||||||
|
|
||||||
PaketModel({
|
PaketModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -19,13 +21,47 @@ class PaketModel {
|
|||||||
required this.deskripsi,
|
required this.deskripsi,
|
||||||
required this.harga,
|
required this.harga,
|
||||||
required this.kuantitas,
|
required this.kuantitas,
|
||||||
required this.foto,
|
this.status = 'aktif',
|
||||||
required this.satuanWaktuSewa,
|
required List<String> foto,
|
||||||
|
required List<Map<String, dynamic>> satuanWaktuSewa,
|
||||||
this.foto_paket,
|
this.foto_paket,
|
||||||
this.images,
|
List<String>? images,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
});
|
}) : foto = List.from(foto),
|
||||||
|
satuanWaktuSewa = List.from(satuanWaktuSewa),
|
||||||
|
images = images != null ? List.from(images) : [];
|
||||||
|
|
||||||
|
// Add copyWith method for immutability patterns
|
||||||
|
PaketModel copyWith({
|
||||||
|
String? id,
|
||||||
|
String? nama,
|
||||||
|
String? deskripsi,
|
||||||
|
double? harga,
|
||||||
|
int? kuantitas,
|
||||||
|
String? status,
|
||||||
|
List<String>? foto,
|
||||||
|
List<Map<String, dynamic>>? satuanWaktuSewa,
|
||||||
|
String? foto_paket,
|
||||||
|
List<String>? images,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
}) {
|
||||||
|
return PaketModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
nama: nama ?? this.nama,
|
||||||
|
deskripsi: deskripsi ?? this.deskripsi,
|
||||||
|
harga: harga ?? this.harga,
|
||||||
|
kuantitas: kuantitas ?? this.kuantitas,
|
||||||
|
status: status ?? this.status,
|
||||||
|
foto: foto ?? List.from(this.foto),
|
||||||
|
satuanWaktuSewa: satuanWaktuSewa ?? List.from(this.satuanWaktuSewa),
|
||||||
|
foto_paket: foto_paket ?? this.foto_paket,
|
||||||
|
images: images ?? (this.images != null ? List.from(this.images!) : null),
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Alias for fromJson to maintain compatibility
|
// Alias for fromJson to maintain compatibility
|
||||||
factory PaketModel.fromMap(Map<String, dynamic> json) => PaketModel.fromJson(json);
|
factory PaketModel.fromMap(Map<String, dynamic> json) => PaketModel.fromJson(json);
|
||||||
@ -63,10 +99,15 @@ class PaketModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
developer.log('📦 [PaketModel.fromJson] Raw status: ${json['status']} (type: ${json['status']?.runtimeType})');
|
||||||
|
final status = json['status']?.toString().toLowerCase() ?? 'aktif';
|
||||||
|
developer.log(' 🏷️ Processed status: $status');
|
||||||
|
|
||||||
return PaketModel(
|
return PaketModel(
|
||||||
id: json['id']?.toString() ?? '',
|
id: json['id']?.toString() ?? '',
|
||||||
nama: json['nama']?.toString() ?? '',
|
nama: json['nama']?.toString() ?? '',
|
||||||
deskripsi: json['deskripsi']?.toString() ?? '',
|
deskripsi: json['deskripsi']?.toString() ?? '',
|
||||||
|
status: status,
|
||||||
harga: (json['harga'] is num) ? (json['harga'] as num).toDouble() : 0.0,
|
harga: (json['harga'] is num) ? (json['harga'] as num).toDouble() : 0.0,
|
||||||
kuantitas: (json['kuantitas'] is num) ? (json['kuantitas'] as num).toInt() : 1,
|
kuantitas: (json['kuantitas'] is num) ? (json['kuantitas'] as num).toInt() : 1,
|
||||||
foto: fotoList,
|
foto: fotoList,
|
||||||
@ -97,34 +138,6 @@ class PaketModel {
|
|||||||
'updated_at': updatedAt.toIso8601String(),
|
'updated_at': updatedAt.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a copy of the model with some fields updated
|
|
||||||
PaketModel copyWith({
|
|
||||||
String? id,
|
|
||||||
String? nama,
|
|
||||||
String? deskripsi,
|
|
||||||
double? harga,
|
|
||||||
int? kuantitas,
|
|
||||||
List<String>? foto,
|
|
||||||
List<Map<String, dynamic>>? satuanWaktuSewa,
|
|
||||||
String? foto_paket,
|
|
||||||
List<String>? images,
|
|
||||||
DateTime? createdAt,
|
|
||||||
DateTime? updatedAt,
|
|
||||||
}) {
|
|
||||||
return PaketModel(
|
|
||||||
id: id ?? this.id,
|
|
||||||
nama: nama ?? this.nama,
|
|
||||||
deskripsi: deskripsi ?? this.deskripsi,
|
|
||||||
harga: harga ?? this.harga,
|
|
||||||
kuantitas: kuantitas ?? this.kuantitas,
|
|
||||||
foto: foto ?? List.from(this.foto),
|
|
||||||
satuanWaktuSewa: satuanWaktuSewa ?? List.from(this.satuanWaktuSewa),
|
|
||||||
foto_paket: foto_paket ?? this.foto_paket,
|
|
||||||
images: images ?? (this.images != null ? List.from(this.images!) : null),
|
|
||||||
createdAt: createdAt ?? this.createdAt,
|
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the first photo URL or a placeholder
|
// Get the first photo URL or a placeholder
|
||||||
String get firstPhotoUrl => foto.isNotEmpty ? foto.first : '';
|
String get firstPhotoUrl => foto.isNotEmpty ? foto.first : '';
|
||||||
|
|||||||
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:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
@ -6,6 +8,8 @@ import '../models/foto_aset_model.dart';
|
|||||||
import '../models/satuan_waktu_model.dart';
|
import '../models/satuan_waktu_model.dart';
|
||||||
import '../models/satuan_waktu_sewa_model.dart';
|
import '../models/satuan_waktu_sewa_model.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import '../models/paket_model.dart';
|
||||||
|
import '../providers/auth_provider.dart';
|
||||||
|
|
||||||
class AsetProvider extends GetxService {
|
class AsetProvider extends GetxService {
|
||||||
late final SupabaseClient client;
|
late final SupabaseClient client;
|
||||||
@ -24,8 +28,16 @@ class AsetProvider extends GetxService {
|
|||||||
.from('aset')
|
.from('aset')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('kategori', 'sewa')
|
.eq('kategori', 'sewa')
|
||||||
.eq('status', 'tersedia') // Hanya yang tersedia
|
.ilike('status', 'tersedia') // Hanya yang tersedia
|
||||||
.order('nama', ascending: true); // Urutan berdasarkan nama
|
.order('nama', ascending: true) // Urutan berdasarkan nama
|
||||||
|
.withConverter<List<Map<String, dynamic>>>(
|
||||||
|
(data) =>
|
||||||
|
data.map<Map<String, dynamic>>((item) {
|
||||||
|
// Ensure 'jenis' is set to 'Sewa' for sewa assets
|
||||||
|
item['jenis'] = 'Sewa';
|
||||||
|
return item;
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
debugPrint('Fetched ${response.length} aset');
|
debugPrint('Fetched ${response.length} aset');
|
||||||
|
|
||||||
@ -56,8 +68,16 @@ class AsetProvider extends GetxService {
|
|||||||
.from('aset')
|
.from('aset')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('kategori', 'langganan')
|
.eq('kategori', 'langganan')
|
||||||
.eq('status', 'tersedia') // Hanya yang tersedia
|
.ilike('status', 'tersedia') // Hanya yang tersedia
|
||||||
.order('nama', ascending: true); // Urutan berdasarkan nama
|
.order('nama', ascending: true) // Urutan berdasarkan nama
|
||||||
|
.withConverter<List<Map<String, dynamic>>>(
|
||||||
|
(data) =>
|
||||||
|
data.map<Map<String, dynamic>>((item) {
|
||||||
|
// Ensure 'jenis' is set to 'Langganan' for langganan assets
|
||||||
|
item['jenis'] = 'Langganan';
|
||||||
|
return item;
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
debugPrint('Fetched ${response.length} langganan aset');
|
debugPrint('Fetched ${response.length} langganan aset');
|
||||||
|
|
||||||
@ -120,9 +140,26 @@ class AsetProvider extends GetxService {
|
|||||||
Future<void> loadAssetPhotos(AsetModel aset) async {
|
Future<void> loadAssetPhotos(AsetModel aset) async {
|
||||||
try {
|
try {
|
||||||
final photos = await getAsetPhotos(aset.id);
|
final photos = await getAsetPhotos(aset.id);
|
||||||
if (photos.isNotEmpty &&
|
if (photos.isNotEmpty) {
|
||||||
(aset.imageUrl == null || aset.imageUrl!.isEmpty)) {
|
// Clear existing images
|
||||||
aset.imageUrl = photos.first.fotoAset;
|
aset.imageUrls.clear();
|
||||||
|
|
||||||
|
// Add all photos to the imageUrls list
|
||||||
|
for (final photo in photos) {
|
||||||
|
if (photo.fotoAset != null && photo.fotoAset!.isNotEmpty) {
|
||||||
|
aset.addImageUrl(photo.fotoAset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the main image URL if it's not already set
|
||||||
|
if ((aset.imageUrl == null || aset.imageUrl!.isEmpty) &&
|
||||||
|
aset.imageUrls.isNotEmpty) {
|
||||||
|
aset.imageUrl = aset.imageUrls.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
'✅ Loaded ${aset.imageUrls.length} photos for asset ${aset.id}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error loading asset photos for ID ${aset.id}: $e');
|
debugPrint('Error loading asset photos for ID ${aset.id}: $e');
|
||||||
@ -172,6 +209,376 @@ class AsetProvider extends GetxService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new asset
|
||||||
|
Future<Map<String, dynamic>?> createAset(
|
||||||
|
Map<String, dynamic> asetData,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
debugPrint('🔄 Creating new aset with data:');
|
||||||
|
asetData.forEach((key, value) {
|
||||||
|
debugPrint(' $key: $value');
|
||||||
|
});
|
||||||
|
|
||||||
|
final response =
|
||||||
|
await client.from('aset').insert(asetData).select().single();
|
||||||
|
|
||||||
|
debugPrint('✅ Aset created successfully with ID: ${response['id']}');
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error creating aset: $e');
|
||||||
|
debugPrint('❌ Stack trace: ${StackTrace.current}');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update an existing asset
|
||||||
|
Future<bool> updateAset(String asetId, Map<String, dynamic> asetData) async {
|
||||||
|
try {
|
||||||
|
debugPrint('🔄 Updating aset with ID: $asetId');
|
||||||
|
asetData.forEach((key, value) {
|
||||||
|
debugPrint(' $key: $value');
|
||||||
|
});
|
||||||
|
|
||||||
|
final response = await client
|
||||||
|
.from('aset')
|
||||||
|
.update(asetData)
|
||||||
|
.eq('id', asetId);
|
||||||
|
|
||||||
|
debugPrint('✅ Aset updated successfully');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error updating aset: $e');
|
||||||
|
debugPrint('❌ Stack trace: ${StackTrace.current}');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a photo URL to the foto_aset table for a specific asset
|
||||||
|
Future<bool> addFotoAset({
|
||||||
|
required String asetId,
|
||||||
|
required String fotoUrl,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
debugPrint('💾 Attempting to save foto to database:');
|
||||||
|
debugPrint(' - asetId: $asetId');
|
||||||
|
debugPrint(' - fotoUrl: $fotoUrl');
|
||||||
|
|
||||||
|
final data = {
|
||||||
|
'id_aset': asetId,
|
||||||
|
'foto_aset': fotoUrl,
|
||||||
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
debugPrint('📤 Inserting into foto_aset table...');
|
||||||
|
final response = await client.from('foto_aset').insert(data).select();
|
||||||
|
|
||||||
|
debugPrint('📝 Database insert response: $response');
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
debugPrint('❌ Failed to insert into foto_aset: Response is null');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response is List && response.isNotEmpty) {
|
||||||
|
debugPrint('✅ Successfully added foto for aset ID: $asetId');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
debugPrint('❌ Failed to add foto: Empty or invalid response');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
debugPrint('❌ Error adding foto aset: $e');
|
||||||
|
debugPrint('Stack trace: $stackTrace');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add satuan waktu sewa for an asset
|
||||||
|
Future<bool> addSatuanWaktuSewa({
|
||||||
|
required String asetId,
|
||||||
|
required String satuanWaktu,
|
||||||
|
required int harga,
|
||||||
|
required int maksimalWaktu,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// First, get the satuan_waktu_id from the satuan_waktu table
|
||||||
|
final response =
|
||||||
|
await client
|
||||||
|
.from('satuan_waktu')
|
||||||
|
.select('id')
|
||||||
|
.ilike('nama_satuan_waktu', satuanWaktu)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
debugPrint('❌ Satuan waktu "$satuanWaktu" not found in the database');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final satuanWaktuId = response['id'] as String;
|
||||||
|
|
||||||
|
final data = {
|
||||||
|
'aset_id': asetId,
|
||||||
|
'satuan_waktu_id': satuanWaktuId,
|
||||||
|
'harga': harga,
|
||||||
|
'maksimal_waktu': maksimalWaktu,
|
||||||
|
};
|
||||||
|
|
||||||
|
debugPrint('🔄 Adding satuan waktu sewa:');
|
||||||
|
data.forEach((key, value) {
|
||||||
|
debugPrint(' $key: $value');
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.from('satuan_waktu_sewa').insert(data);
|
||||||
|
debugPrint('✅ Satuan waktu sewa added successfully');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error adding satuan waktu sewa: $e');
|
||||||
|
debugPrint('❌ Stack trace: ${StackTrace.current}');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all satuan waktu sewa for an asset
|
||||||
|
Future<bool> deleteSatuanWaktuSewaByAsetId(String asetId) async {
|
||||||
|
try {
|
||||||
|
await client
|
||||||
|
.from('satuan_waktu_sewa')
|
||||||
|
.delete()
|
||||||
|
.eq('aset_id', asetId); // Changed from 'id_aset' to 'aset_id'
|
||||||
|
debugPrint('✅ Deleted satuan waktu sewa for aset ID: $asetId');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error deleting satuan waktu sewa: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uploads a file to Supabase Storage root
|
||||||
|
/// Returns the public URL of the uploaded file, or null if upload fails
|
||||||
|
Future<String?> uploadFileToStorage(File file) async {
|
||||||
|
try {
|
||||||
|
if (!await file.exists()) {
|
||||||
|
debugPrint('❌ File does not exist: ${file.path}');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final fileName =
|
||||||
|
'${DateTime.now().millisecondsSinceEpoch}_${file.path.split(Platform.pathSeparator).last}';
|
||||||
|
debugPrint('🔄 Preparing to upload file: $fileName');
|
||||||
|
|
||||||
|
final uploadResponse = await client.storage
|
||||||
|
.from('foto.aset')
|
||||||
|
.upload(fileName, file, fileOptions: FileOptions(upsert: true));
|
||||||
|
|
||||||
|
debugPrint('📤 Upload response: $uploadResponse');
|
||||||
|
|
||||||
|
final publicUrl = client.storage.from('foto.aset').getPublicUrl(fileName);
|
||||||
|
|
||||||
|
debugPrint('✅ File uploaded successfully. Public URL: $publicUrl');
|
||||||
|
return publicUrl;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
debugPrint('❌ Error uploading file to storage: $e');
|
||||||
|
debugPrint('Stack trace: $stackTrace');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper method to delete a file from Supabase Storage
|
||||||
|
Future<bool> deleteFileFromStorage(String fileUrl) async {
|
||||||
|
try {
|
||||||
|
debugPrint('🔄 Preparing to delete file from storage');
|
||||||
|
|
||||||
|
// Extract the file path from the full URL
|
||||||
|
final uri = Uri.parse(fileUrl);
|
||||||
|
final pathSegments = uri.pathSegments;
|
||||||
|
|
||||||
|
// Find the index of 'foto.aset' in the path
|
||||||
|
final fotoAsetIndex = pathSegments.indexWhere(
|
||||||
|
(segment) => segment == 'foto.aset',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fotoAsetIndex == -1 || fotoAsetIndex == pathSegments.length - 1) {
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ Invalid file URL format, cannot extract file path: $fileUrl',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the file path relative to the bucket
|
||||||
|
final filePath = pathSegments.sublist(fotoAsetIndex + 1).join('/');
|
||||||
|
|
||||||
|
debugPrint('🗑️ Deleting file from storage - Path: $filePath');
|
||||||
|
|
||||||
|
// Delete the file from storage
|
||||||
|
await client.storage.from('foto.aset').remove([filePath]);
|
||||||
|
|
||||||
|
debugPrint('✅ Successfully deleted file from storage');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error deleting file from storage: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the photos for an asset
|
||||||
|
/// Handles both local file uploads and existing URLs
|
||||||
|
/// Returns true if all operations were successful
|
||||||
|
Future<bool> updateFotoAset({
|
||||||
|
required String asetId,
|
||||||
|
required List<String> fotoUrls,
|
||||||
|
}) async {
|
||||||
|
if (fotoUrls.isEmpty) {
|
||||||
|
debugPrint('ℹ️ No photos to update for asset: $asetId');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugPrint('🔄 Starting photo update for asset: $asetId');
|
||||||
|
|
||||||
|
// 1. Get existing photo URLs before deleting them
|
||||||
|
debugPrint('📋 Fetching existing photos for asset: $asetId');
|
||||||
|
final existingPhotos = await client
|
||||||
|
.from('foto_aset')
|
||||||
|
.select('foto_aset')
|
||||||
|
.eq('id_aset', asetId);
|
||||||
|
|
||||||
|
// 2. Delete files from storage first
|
||||||
|
if (existingPhotos is List && existingPhotos.isNotEmpty) {
|
||||||
|
debugPrint('🗑️ Deleting ${existingPhotos.length} files from storage');
|
||||||
|
for (final photo in existingPhotos) {
|
||||||
|
final url = photo['foto_aset'] as String?;
|
||||||
|
if (url != null && url.isNotEmpty) {
|
||||||
|
await deleteFileFromStorage(url);
|
||||||
|
} else {
|
||||||
|
debugPrint('⚠️ Skipping invalid photo URL: $photo');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint('ℹ️ No existing photos found in database');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Remove duplicates from new fotoUrls
|
||||||
|
final uniqueFotoUrls = fotoUrls.toSet().toList();
|
||||||
|
debugPrint(
|
||||||
|
'📸 Processing ${uniqueFotoUrls.length} unique photos (was ${fotoUrls.length})',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Delete existing photo records from database
|
||||||
|
debugPrint('🗑️ Removing existing photo records from database');
|
||||||
|
try {
|
||||||
|
final deleteResponse = await client
|
||||||
|
.from('foto_aset')
|
||||||
|
.delete()
|
||||||
|
.eq('id_aset', asetId);
|
||||||
|
|
||||||
|
debugPrint('🗑️ Database delete response: $deleteResponse');
|
||||||
|
|
||||||
|
// Verify deletion
|
||||||
|
final remainingPhotos = await client
|
||||||
|
.from('foto_aset')
|
||||||
|
.select()
|
||||||
|
.eq('id_aset', asetId);
|
||||||
|
|
||||||
|
if (remainingPhotos is List && remainingPhotos.isNotEmpty) {
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ Warning: ${remainingPhotos.length} photos still exist in database after delete',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error deleting existing photo records: $e');
|
||||||
|
// Continue with the update even if deletion fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Process each unique new photo
|
||||||
|
bool allSuccess = true;
|
||||||
|
int processedCount = 0;
|
||||||
|
|
||||||
|
for (final fotoUrl in uniqueFotoUrls) {
|
||||||
|
if (fotoUrl.isEmpty) {
|
||||||
|
debugPrint('⏭️ Skipping empty photo URL');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugPrint(
|
||||||
|
'\n🔄 Processing photo ${processedCount + 1}/${uniqueFotoUrls.length}: ${fotoUrl.length > 50 ? '${fotoUrl.substring(0, 50)}...' : fotoUrl}',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if it's a local file
|
||||||
|
if (fotoUrl.startsWith('file://') ||
|
||||||
|
fotoUrl.startsWith('/') ||
|
||||||
|
!fotoUrl.startsWith('http')) {
|
||||||
|
final file = File(fotoUrl.replaceFirst('file://', ''));
|
||||||
|
if (!await file.exists()) {
|
||||||
|
debugPrint('❌ File does not exist: ${file.path}');
|
||||||
|
allSuccess = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('📤 Uploading local file...');
|
||||||
|
final uploadedUrl = await uploadFileToStorage(file);
|
||||||
|
|
||||||
|
if (uploadedUrl == null) {
|
||||||
|
debugPrint('❌ Failed to upload file');
|
||||||
|
allSuccess = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('💾 Saving to database...');
|
||||||
|
final success = await addFotoAset(
|
||||||
|
asetId: asetId,
|
||||||
|
fotoUrl: uploadedUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
processedCount++;
|
||||||
|
debugPrint('✅ Successfully saved photo #$processedCount');
|
||||||
|
} else {
|
||||||
|
allSuccess = false;
|
||||||
|
debugPrint('❌ Failed to save photo URL to database');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Skip placeholder values
|
||||||
|
else if (fotoUrl == 'pending_upload') {
|
||||||
|
debugPrint('⏭️ Skipping placeholder URL');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Handle existing URLs
|
||||||
|
else if (fotoUrl.startsWith('http')) {
|
||||||
|
debugPrint('🌐 Processing existing URL...');
|
||||||
|
final success = await addFotoAset(asetId: asetId, fotoUrl: fotoUrl);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
processedCount++;
|
||||||
|
debugPrint('✅ Successfully saved URL #$processedCount');
|
||||||
|
} else {
|
||||||
|
allSuccess = false;
|
||||||
|
debugPrint('❌ Failed to save URL to database');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint('⚠️ Unrecognized URL format, skipping');
|
||||||
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
allSuccess = false;
|
||||||
|
debugPrint('❌ Error processing photo: $e');
|
||||||
|
debugPrint('Stack trace: $stackTrace');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('\n📊 Photo update complete');
|
||||||
|
debugPrint('✅ Success: $allSuccess');
|
||||||
|
debugPrint(
|
||||||
|
'📸 Processed: $processedCount/${uniqueFotoUrls.length} unique photos',
|
||||||
|
);
|
||||||
|
|
||||||
|
return allSuccess && processedCount > 0;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error updating foto aset: $e');
|
||||||
|
debugPrint('Stack trace: ${StackTrace.current}');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve bookings for a specific asset on a specific date
|
// Retrieve bookings for a specific asset on a specific date
|
||||||
Future<List<Map<String, dynamic>>> getAsetBookings(
|
Future<List<Map<String, dynamic>>> getAsetBookings(
|
||||||
String asetId,
|
String asetId,
|
||||||
@ -1061,7 +1468,9 @@ class AsetProvider extends GetxService {
|
|||||||
.order('created_at');
|
.order('created_at');
|
||||||
|
|
||||||
if (response != null && response.isNotEmpty) {
|
if (response != null && response.isNotEmpty) {
|
||||||
return response.map<String>((item) => item['foto_aset'] as String).toList();
|
return response
|
||||||
|
.map<String>((item) => item['foto_aset'] as String)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -1095,16 +1504,22 @@ class AsetProvider extends GetxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response.isEmpty) {
|
if (response.isEmpty) {
|
||||||
debugPrint('ℹ️ [INFO] No items found in paket_item for paket ID: $paketId');
|
debugPrint(
|
||||||
|
'ℹ️ [INFO] No items found in paket_item for paket ID: $paketId',
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('✅ [SUCCESS] Found ${response.length} items in paket_item');
|
debugPrint(
|
||||||
|
'✅ [SUCCESS] Found [1m${response.length}[0m items in paket_item',
|
||||||
|
);
|
||||||
|
|
||||||
final List<Map<String, dynamic>> enrichedItems = [];
|
final List<Map<String, dynamic>> enrichedItems = [];
|
||||||
|
|
||||||
// Process each item to fetch additional details
|
// Process each item to fetch additional details
|
||||||
debugPrint('🔄 [3/3] Processing ${response.length} items to fetch asset details');
|
debugPrint(
|
||||||
|
'🔄 [3/3] Processing ${response.length} items to fetch asset details',
|
||||||
|
);
|
||||||
|
|
||||||
for (var item in response) {
|
for (var item in response) {
|
||||||
final String? asetId = item['aset_id']?.toString();
|
final String? asetId = item['aset_id']?.toString();
|
||||||
@ -1123,13 +1538,16 @@ class AsetProvider extends GetxService {
|
|||||||
try {
|
try {
|
||||||
// 1. Get asset name from aset table
|
// 1. Get asset name from aset table
|
||||||
debugPrint(' - Querying aset table for id: $asetId');
|
debugPrint(' - Querying aset table for id: $asetId');
|
||||||
final asetResponse = await client
|
final asetResponse =
|
||||||
|
await client
|
||||||
.from('aset')
|
.from('aset')
|
||||||
.select('id, nama, deskripsi')
|
.select('id, nama, deskripsi')
|
||||||
.eq('id', asetId)
|
.eq('id', asetId)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
debugPrint(' - Aset response: ${asetResponse?.toString() ?? 'null'}');
|
debugPrint(
|
||||||
|
' - Aset response: ${asetResponse?.toString() ?? 'null'}',
|
||||||
|
);
|
||||||
|
|
||||||
if (asetResponse == null) {
|
if (asetResponse == null) {
|
||||||
debugPrint('⚠️ [WARNING] No asset found with id: $asetId');
|
debugPrint('⚠️ [WARNING] No asset found with id: $asetId');
|
||||||
@ -1139,7 +1557,7 @@ class AsetProvider extends GetxService {
|
|||||||
'nama_aset': 'Item tidak diketahui',
|
'nama_aset': 'Item tidak diketahui',
|
||||||
'foto_aset': '',
|
'foto_aset': '',
|
||||||
'semua_foto': <String>[],
|
'semua_foto': <String>[],
|
||||||
'error': 'Asset not found'
|
'error': 'Asset not found',
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1173,15 +1591,18 @@ class AsetProvider extends GetxService {
|
|||||||
final enrichedItem = {
|
final enrichedItem = {
|
||||||
'aset_id': asetId,
|
'aset_id': asetId,
|
||||||
'kuantitas': kuantitas,
|
'kuantitas': kuantitas,
|
||||||
'nama_aset': asetResponse['nama']?.toString() ?? 'Nama tidak tersedia',
|
'nama_aset':
|
||||||
|
asetResponse['nama']?.toString() ?? 'Nama tidak tersedia',
|
||||||
'foto_aset': fotoUtama,
|
'foto_aset': fotoUtama,
|
||||||
'semua_foto': semuaFoto,
|
'semua_foto': semuaFoto,
|
||||||
'debug': {
|
'debug': {
|
||||||
'aset_query': asetResponse,
|
'aset_query': asetResponse,
|
||||||
'foto_count': semuaFoto.length
|
'foto_count': semuaFoto.length,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
debugPrint('✅ [ENRICHED ITEM] $enrichedItem');
|
||||||
|
|
||||||
enrichedItems.add(enrichedItem);
|
enrichedItems.add(enrichedItem);
|
||||||
|
|
||||||
// Debug log
|
// Debug log
|
||||||
@ -1193,7 +1614,6 @@ class AsetProvider extends GetxService {
|
|||||||
if (semuaFoto.isNotEmpty) {
|
if (semuaFoto.isNotEmpty) {
|
||||||
debugPrint(' - Foto Utama: ${semuaFoto.first}');
|
debugPrint(' - Foto Utama: ${semuaFoto.first}');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error processing asset $asetId: $e');
|
debugPrint('❌ Error processing asset $asetId: $e');
|
||||||
// Still add the basic item even if we couldn't fetch additional details
|
// Still add the basic item even if we couldn't fetch additional details
|
||||||
@ -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;
|
return enrichedItems;
|
||||||
|
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
debugPrint('❌ Error getting package items for paket $paketId: $e');
|
debugPrint('❌ Error getting package items for paket $paketId: $e');
|
||||||
debugPrint('Stack trace: $stackTrace');
|
debugPrint('Stack trace: $stackTrace');
|
||||||
@ -1221,10 +1645,9 @@ class AsetProvider extends GetxService {
|
|||||||
Future<List<Map<String, dynamic>>> getBankAccounts() async {
|
Future<List<Map<String, dynamic>>> getBankAccounts() async {
|
||||||
try {
|
try {
|
||||||
final response = await client
|
final response = await client
|
||||||
.from('bank_accounts')
|
.from('akun_bank')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('is_active', true)
|
.order('nama_bank');
|
||||||
.order('bank_name');
|
|
||||||
|
|
||||||
if (response != null && response.isNotEmpty) {
|
if (response != null && response.isNotEmpty) {
|
||||||
return List<Map<String, dynamic>>.from(response);
|
return List<Map<String, dynamic>>.from(response);
|
||||||
@ -1235,4 +1658,325 @@ class AsetProvider extends GetxService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch all packages with their related data (photos and rental time units)
|
||||||
|
Future<List<PaketModel>> getAllPaket() async {
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
|
final String debugId = DateTime.now().millisecondsSinceEpoch
|
||||||
|
.toString()
|
||||||
|
.substring(8);
|
||||||
|
|
||||||
|
void log(String message, {bool isError = false, bool isSection = false}) {
|
||||||
|
final prefix =
|
||||||
|
isError
|
||||||
|
? '❌'
|
||||||
|
: isSection
|
||||||
|
? '📌'
|
||||||
|
: ' ';
|
||||||
|
debugPrint('[$debugId] $prefix $message');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log('🚀 Memulai pengambilan data paket...', isSection: true);
|
||||||
|
log('📡 Mengambil data paket dari database...');
|
||||||
|
|
||||||
|
// 1) Get all packages
|
||||||
|
final paketResponse = await client
|
||||||
|
.from('paket')
|
||||||
|
.select('*')
|
||||||
|
.order('created_at', ascending: false);
|
||||||
|
|
||||||
|
log('📥 Diterima ${paketResponse.length} paket dari database');
|
||||||
|
|
||||||
|
if (paketResponse.isEmpty) {
|
||||||
|
log('ℹ️ Tidak ada paket yang ditemukan');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to list of PaketModel (without relations yet)
|
||||||
|
log('\n🔍 Memproses data paket...');
|
||||||
|
final List<PaketModel> paketList = [];
|
||||||
|
int successCount = 0;
|
||||||
|
|
||||||
|
for (var p in paketResponse) {
|
||||||
|
try {
|
||||||
|
final paket = PaketModel.fromMap(p as Map<String, dynamic>);
|
||||||
|
paketList.add(paket);
|
||||||
|
successCount++;
|
||||||
|
log(' ✅ Berhasil memproses paket: ${paket.id} - ${paket.nama}');
|
||||||
|
} catch (e) {
|
||||||
|
log('⚠️ Gagal memproses paket: $e', isError: true);
|
||||||
|
log(' Data paket: $p');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log('\n📊 Ringkasan Pemrosesan:');
|
||||||
|
log(' - Total data: ${paketResponse.length}');
|
||||||
|
log(' - Berhasil: $successCount');
|
||||||
|
log(' - Gagal: ${paketResponse.length - successCount}');
|
||||||
|
|
||||||
|
if (paketList.isEmpty) {
|
||||||
|
log('ℹ️ Tidak ada paket yang valid setelah diproses');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kumpulkan semua ID paket
|
||||||
|
final List<String> paketIds = paketList.map((p) => p.id).toList();
|
||||||
|
log('\n📦 Mengambil data tambahan untuk ${paketList.length} paket...');
|
||||||
|
log(' ID Paket: ${paketIds.join(', ')}');
|
||||||
|
|
||||||
|
// 2) Ambil semua foto untuk paket-paket ini
|
||||||
|
log('\n🖼️ Mengambil data foto...');
|
||||||
|
|
||||||
|
final fotoResp = await client
|
||||||
|
.from('foto_aset')
|
||||||
|
.select('id_paket, foto_aset')
|
||||||
|
.inFilter('id_paket', paketIds);
|
||||||
|
|
||||||
|
log(' Ditemukan ${fotoResp.length} foto');
|
||||||
|
|
||||||
|
// Map packageId -> List<String> photos
|
||||||
|
final Map<String, List<String>> mapFoto = {};
|
||||||
|
int fotoCount = 0;
|
||||||
|
|
||||||
|
for (var row in fotoResp) {
|
||||||
|
try {
|
||||||
|
final pid = row['id_paket']?.toString() ?? '';
|
||||||
|
final url = row['foto_aset']?.toString() ?? '';
|
||||||
|
if (pid.isNotEmpty && url.isNotEmpty) {
|
||||||
|
mapFoto.putIfAbsent(pid, () => []).add(url);
|
||||||
|
fotoCount++;
|
||||||
|
} else {
|
||||||
|
log(' ⚠️ Data foto tidak valid: ${row.toString()}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log('⚠️ Gagal memproses data foto: $e', isError: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(' Berhasil memetakan $fotoCount foto ke ${mapFoto.length} paket');
|
||||||
|
|
||||||
|
// 3) Get all satuan_waktu_sewa for these packages
|
||||||
|
log('\n⏱️ Mengambil data satuan waktu sewa...');
|
||||||
|
|
||||||
|
final swsResp = await client
|
||||||
|
.from('satuan_waktu_sewa')
|
||||||
|
.select('paket_id, satuan_waktu_id, harga, maksimal_waktu')
|
||||||
|
.inFilter('paket_id', paketIds);
|
||||||
|
|
||||||
|
log(' Ditemukan ${swsResp.length} entri satuan waktu sewa');
|
||||||
|
|
||||||
|
// Process satuan waktu sewa
|
||||||
|
final Map<String, List<Map<String, dynamic>>> paketSatuanWaktu = {};
|
||||||
|
int swsCount = 0;
|
||||||
|
|
||||||
|
for (var row in swsResp) {
|
||||||
|
try {
|
||||||
|
final pid = row['paket_id']?.toString() ?? '';
|
||||||
|
if (pid.isNotEmpty) {
|
||||||
|
final swsData = {
|
||||||
|
'satuan_waktu_id': row['satuan_waktu_id'],
|
||||||
|
'harga': row['harga'],
|
||||||
|
'maksimal_waktu': row['maksimal_waktu'],
|
||||||
|
};
|
||||||
|
paketSatuanWaktu.putIfAbsent(pid, () => []).add(swsData);
|
||||||
|
swsCount++;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log('⚠️ Gagal memproses satuan waktu sewa: $e', isError: true);
|
||||||
|
log(' Data: $row');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(
|
||||||
|
' Berhasil memetakan $swsCount satuan waktu ke ${paketSatuanWaktu.length} paket',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4) Gabungkan semua data
|
||||||
|
log('\n🔗 Menggabungkan data...');
|
||||||
|
final List<PaketModel> result = [];
|
||||||
|
int combinedCount = 0;
|
||||||
|
|
||||||
|
for (var paket in paketList) {
|
||||||
|
final pid = paket.id;
|
||||||
|
log('\n📦 Memproses paket: ${paket.nama} ($pid)');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final updatedPaket = paket.copyWith();
|
||||||
|
|
||||||
|
// Lampirkan foto
|
||||||
|
if (mapFoto.containsKey(pid)) {
|
||||||
|
final fotoList = mapFoto[pid]!;
|
||||||
|
updatedPaket.images = List<String>.from(fotoList);
|
||||||
|
|
||||||
|
// Set foto utama jika belum ada
|
||||||
|
if (updatedPaket.images!.isNotEmpty &&
|
||||||
|
updatedPaket.foto_paket == null) {
|
||||||
|
updatedPaket.foto_paket = updatedPaket.images!.first;
|
||||||
|
log(' 📷 Menambahkan ${fotoList.length} foto');
|
||||||
|
log(' 🖼️ Foto utama: ${updatedPaket.foto_paket}');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(' ℹ️ Tidak ada foto untuk paket ini');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lampirkan satuan waktu sewa
|
||||||
|
if (paketSatuanWaktu.containsKey(pid)) {
|
||||||
|
final swsList = List<Map<String, dynamic>>.from(
|
||||||
|
paketSatuanWaktu[pid] ?? [],
|
||||||
|
);
|
||||||
|
updatedPaket.satuanWaktuSewa = swsList;
|
||||||
|
log(' ⏱️ Menambahkan ${swsList.length} satuan waktu sewa');
|
||||||
|
|
||||||
|
// Log detail harga
|
||||||
|
for (var sws in swsList.take(2)) {
|
||||||
|
// Tampilkan maksimal 2 harga
|
||||||
|
log(
|
||||||
|
' - ${sws['harga']} / satuan waktu (ID: ${sws['satuan_waktu_id']})',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (swsList.length > 2) {
|
||||||
|
log(' - ...dan ${swsList.length - 2} lainnya');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(' ℹ️ Tidak ada satuan waktu sewa untuk paket ini');
|
||||||
|
}
|
||||||
|
|
||||||
|
result.add(updatedPaket);
|
||||||
|
combinedCount++;
|
||||||
|
log(' ✅ Berhasil memproses paket $pid');
|
||||||
|
} catch (e) {
|
||||||
|
log('⚠️ Gagal memproses paket $pid: $e', isError: true);
|
||||||
|
// Tetap tambahkan paket asli jika gagal diproses
|
||||||
|
result.add(paket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ringkasan eksekusi
|
||||||
|
stopwatch.stop();
|
||||||
|
log('\n🎉 Selesai!', isSection: true);
|
||||||
|
log('📊 Ringkasan Eksekusi:');
|
||||||
|
log(' - Total paket: ${paketList.length}');
|
||||||
|
log(' - Berhasil diproses: $combinedCount/${paketList.length}');
|
||||||
|
log(' - Total foto: $fotoCount');
|
||||||
|
log(' - Total satuan waktu: $swsCount');
|
||||||
|
log(' - Waktu eksekusi: ${stopwatch.elapsedMilliseconds}ms');
|
||||||
|
log(' - ID Debug: $debugId');
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
log('\n❌ ERROR KRITIS', isError: true);
|
||||||
|
log('Pesan error: $e', isError: true);
|
||||||
|
log('Stack trace: $stackTrace', isError: true);
|
||||||
|
log('ID Debug: $debugId', isError: true);
|
||||||
|
rethrow;
|
||||||
|
debugPrint('❌ [getAllPaket] Error: $e');
|
||||||
|
debugPrint('Stack trace: $stackTrace');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update tagihan_dibayar and insert pembayaran
|
||||||
|
Future<bool> processPembayaranTagihan({
|
||||||
|
required String tagihanSewaId,
|
||||||
|
required int nominal,
|
||||||
|
required String metodePembayaran,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// 1. Get current tagihan_dibayar
|
||||||
|
final tagihan =
|
||||||
|
await client
|
||||||
|
.from('tagihan_sewa')
|
||||||
|
.select('tagihan_dibayar')
|
||||||
|
.eq('id', tagihanSewaId)
|
||||||
|
.maybeSingle();
|
||||||
|
int currentDibayar = 0;
|
||||||
|
if (tagihan != null && tagihan['tagihan_dibayar'] != null) {
|
||||||
|
currentDibayar =
|
||||||
|
int.tryParse(tagihan['tagihan_dibayar'].toString()) ?? 0;
|
||||||
|
}
|
||||||
|
final newDibayar = currentDibayar + nominal;
|
||||||
|
|
||||||
|
// 2. Update tagihan_dibayar
|
||||||
|
await client
|
||||||
|
.from('tagihan_sewa')
|
||||||
|
.update({'tagihan_dibayar': newDibayar})
|
||||||
|
.eq('id', tagihanSewaId);
|
||||||
|
|
||||||
|
// 3. Insert pembayaran
|
||||||
|
final authProvider = Get.find<AuthProvider>();
|
||||||
|
final idPetugas = authProvider.getCurrentUserId();
|
||||||
|
final pembayaranData = {
|
||||||
|
'tagihan_sewa_id': tagihanSewaId,
|
||||||
|
'metode_pembayaran': metodePembayaran,
|
||||||
|
'total_pembayaran': nominal,
|
||||||
|
'status': 'lunas',
|
||||||
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
|
'id_petugas': idPetugas,
|
||||||
|
};
|
||||||
|
await client.from('pembayaran').insert(pembayaranData);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error processing pembayaran tagihan: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status of sewa_aset by ID
|
||||||
|
Future<bool> updateSewaAsetStatus({
|
||||||
|
required String sewaAsetId,
|
||||||
|
required String status,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
debugPrint('🔄 Updating status of sewa_aset ID: $sewaAsetId to $status');
|
||||||
|
final response = await client
|
||||||
|
.from('sewa_aset')
|
||||||
|
.update({'status': status})
|
||||||
|
.eq('id', sewaAsetId);
|
||||||
|
debugPrint('✅ Status updated for sewa_aset ID: $sewaAsetId');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error updating sewa_aset status: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all payment proof image URLs for a sewa_aset (by tagihan_sewa)
|
||||||
|
Future<List<String>> getFotoPembayaranUrlsByTagihanSewaId(
|
||||||
|
String sewaAsetId,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
// 1. Get tagihan_sewa by sewaAsetId
|
||||||
|
final tagihan = await getTagihanSewa(sewaAsetId);
|
||||||
|
if (tagihan == null || tagihan['id'] == null) return [];
|
||||||
|
final tagihanSewaId = tagihan['id'];
|
||||||
|
// 2. Fetch all foto_pembayaran for this tagihan_sewa_id
|
||||||
|
final List<dynamic> response = await client
|
||||||
|
.from('foto_pembayaran')
|
||||||
|
.select('foto_pembayaran')
|
||||||
|
.eq('tagihan_sewa_id', tagihanSewaId)
|
||||||
|
.order('created_at', ascending: false);
|
||||||
|
// 3. Extract URLs
|
||||||
|
return response
|
||||||
|
.map<String>((row) => row['foto_pembayaran']?.toString() ?? '')
|
||||||
|
.where((url) => url.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error fetching foto pembayaran: $e');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> countSewaAsetByStatus(List<String> statuses) async {
|
||||||
|
// Supabase expects the IN filter as a comma-separated string in parentheses
|
||||||
|
final statusString = '(${statuses.map((s) => '"$s"').join(',')})';
|
||||||
|
final response = await client
|
||||||
|
.from('sewa_aset')
|
||||||
|
.select('id')
|
||||||
|
.filter('status', 'in', statusString);
|
||||||
|
if (response is List) {
|
||||||
|
return response.length;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
lib/app/main.dart
Normal file
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 emailController = TextEditingController();
|
||||||
final passwordController = TextEditingController();
|
final passwordController = TextEditingController();
|
||||||
|
final formKey = GlobalKey<FormState>();
|
||||||
|
final nameController = TextEditingController();
|
||||||
|
final confirmPasswordController = TextEditingController();
|
||||||
|
final RxBool isConfirmPasswordVisible = false.obs;
|
||||||
|
|
||||||
// Form fields for registration
|
// Form fields for registration
|
||||||
final RxString email = ''.obs;
|
final RxString email = ''.obs;
|
||||||
@ -15,6 +19,7 @@ class AuthController extends GetxController {
|
|||||||
final RxString nik = ''.obs;
|
final RxString nik = ''.obs;
|
||||||
final RxString phoneNumber = ''.obs;
|
final RxString phoneNumber = ''.obs;
|
||||||
final RxString selectedRole = 'WARGA'.obs; // Default role
|
final RxString selectedRole = 'WARGA'.obs; // Default role
|
||||||
|
final RxString alamatLengkap = ''.obs;
|
||||||
|
|
||||||
// Form status
|
// Form status
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
@ -28,6 +33,10 @@ class AuthController extends GetxController {
|
|||||||
isPasswordVisible.value = !isPasswordVisible.value;
|
isPasswordVisible.value = !isPasswordVisible.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void toggleConfirmPasswordVisibility() {
|
||||||
|
isConfirmPasswordVisible.value = !isConfirmPasswordVisible.value;
|
||||||
|
}
|
||||||
|
|
||||||
// Change role selection
|
// Change role selection
|
||||||
void setRole(String? role) {
|
void setRole(String? role) {
|
||||||
if (role != null) {
|
if (role != null) {
|
||||||
@ -172,6 +181,8 @@ class AuthController extends GetxController {
|
|||||||
void onClose() {
|
void onClose() {
|
||||||
emailController.dispose();
|
emailController.dispose();
|
||||||
passwordController.dispose();
|
passwordController.dispose();
|
||||||
|
nameController.dispose();
|
||||||
|
confirmPasswordController.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,7 +192,8 @@ class AuthController extends GetxController {
|
|||||||
if (email.value.isEmpty ||
|
if (email.value.isEmpty ||
|
||||||
password.value.isEmpty ||
|
password.value.isEmpty ||
|
||||||
nik.value.isEmpty ||
|
nik.value.isEmpty ||
|
||||||
phoneNumber.value.isEmpty) {
|
phoneNumber.value.isEmpty ||
|
||||||
|
alamatLengkap.value.isEmpty) {
|
||||||
errorMessage.value = 'Semua field harus diisi';
|
errorMessage.value = 'Semua field harus diisi';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -222,6 +234,7 @@ class AuthController extends GetxController {
|
|||||||
data: {
|
data: {
|
||||||
'nik': nik.value.trim(),
|
'nik': nik.value.trim(),
|
||||||
'phone_number': phoneNumber.value.trim(),
|
'phone_number': phoneNumber.value.trim(),
|
||||||
|
'alamat_lengkap': alamatLengkap.value.trim(),
|
||||||
'role': selectedRole.value,
|
'role': selectedRole.value,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -26,12 +26,8 @@ class ForgotPasswordView extends GetView<AuthController> {
|
|||||||
Opacity(
|
Opacity(
|
||||||
opacity: 0.03,
|
opacity: 0.03,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
color: Colors.blue[50], // Temporary solid color
|
||||||
image: AssetImage('assets/images/pattern.png'),
|
|
||||||
repeat: ImageRepeat.repeat,
|
|
||||||
scale: 4.0,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -30,12 +30,8 @@ class LoginView extends GetView<AuthController> {
|
|||||||
Opacity(
|
Opacity(
|
||||||
opacity: 0.03,
|
opacity: 0.03,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
color: Colors.blue[50], // Temporary solid color
|
||||||
image: AssetImage('assets/images/pattern.png'),
|
|
||||||
repeat: ImageRepeat.repeat,
|
|
||||||
scale: 4.0,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -89,7 +85,6 @@ class LoginView extends GetView<AuthController> {
|
|||||||
_buildHeader(),
|
_buildHeader(),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
_buildLoginCard(),
|
_buildLoginCard(),
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildRegisterLink(),
|
_buildRegisterLink(),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
],
|
],
|
||||||
@ -161,7 +156,7 @@ class LoginView extends GetView<AuthController> {
|
|||||||
prefixIcon: Icons.email_outlined,
|
prefixIcon: Icons.email_outlined,
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// Password field
|
// Password field
|
||||||
_buildInputLabel('Password'),
|
_buildInputLabel('Password'),
|
||||||
@ -204,7 +199,6 @@ class LoginView extends GetView<AuthController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
|
||||||
|
|
||||||
// Login button
|
// Login button
|
||||||
Obx(
|
Obx(
|
||||||
|
|||||||
@ -187,7 +187,7 @@ class RegistrationView extends GetView<AuthController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Pendaftaran hanya dapat dilakukan oleh warga dan mitra yang sudah terverivikasi. Silahkan hubungi petugas atau kunjungi kantor untuk informasi lebih lanjut.',
|
'Setelah pendaftaran lengkapi data diri untuk dapat melakukan sewa',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
@ -203,121 +203,32 @@ class RegistrationView extends GetView<AuthController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRegistrationForm() {
|
Widget _buildRegistrationForm() {
|
||||||
return Column(
|
return Form(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
key: controller.formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// Email Input
|
|
||||||
_buildInputLabel('Email'),
|
_buildInputLabel('Email'),
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildEmailField(),
|
_buildEmailField(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Password Input
|
|
||||||
_buildInputLabel('Password'),
|
_buildInputLabel('Password'),
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildPasswordField(),
|
_buildPasswordField(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 16),
|
||||||
|
_buildInputLabel('Konfirmasi Password'),
|
||||||
// NIK Input
|
_buildConfirmPasswordField(),
|
||||||
_buildInputLabel('NIK'),
|
const SizedBox(height: 16),
|
||||||
const SizedBox(height: 8),
|
_buildInputLabel('Nama Lengkap'),
|
||||||
_buildNikField(),
|
_buildNameField(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 16),
|
||||||
|
_buildInputLabel('No HP'),
|
||||||
// Phone Number Input
|
|
||||||
_buildInputLabel('No. Hp'),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildPhoneField(),
|
_buildPhoneField(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 16),
|
||||||
|
_buildInputLabel('Alamat Lengkap'),
|
||||||
// Role Selection Dropdown
|
_buildAlamatField(),
|
||||||
Column(
|
const SizedBox(height: 16),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
// Removed: NIK, No HP, and Dropdown Daftar Sebagai
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'Daftar Sebagai',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[100],
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: Colors.grey[300]!, width: 1),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Obx(
|
|
||||||
() => DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton<String>(
|
|
||||||
isExpanded: true,
|
|
||||||
value: controller.selectedRole.value,
|
|
||||||
hint: const Text('Pilih Peran'),
|
|
||||||
items: [
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: 'WARGA',
|
|
||||||
child: const Text('Warga'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: 'PETUGAS_MITRA',
|
|
||||||
child: const Text('Mitra'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onChanged: (value) {
|
|
||||||
controller.setRole(value);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.arrow_drop_down),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.black87,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Error message
|
|
||||||
Obx(
|
|
||||||
() =>
|
|
||||||
controller.errorMessage.value.isNotEmpty
|
|
||||||
? Container(
|
|
||||||
margin: const EdgeInsets.only(top: 8),
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.errorLight,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.error_outline,
|
|
||||||
color: AppColors.error,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
controller.errorMessage.value,
|
|
||||||
style: TextStyle(
|
|
||||||
color: AppColors.error,
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,78 +326,101 @@ class RegistrationView extends GetView<AuthController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNikField() {
|
Widget _buildConfirmPasswordField() {
|
||||||
return Container(
|
return Obx(
|
||||||
decoration: BoxDecoration(
|
() => TextFormField(
|
||||||
color: AppColors.surface,
|
controller: controller.confirmPasswordController,
|
||||||
borderRadius: BorderRadius.circular(16),
|
obscureText: !controller.isConfirmPasswordVisible.value,
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: AppColors.shadow,
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
onChanged: (value) => controller.nik.value = value,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Masukkan NIK anda',
|
hintText: 'Masukkan ulang password anda',
|
||||||
hintStyle: TextStyle(color: AppColors.textLight),
|
suffixIcon: IconButton(
|
||||||
prefixIcon: Icon(
|
icon: Icon(
|
||||||
Icons.credit_card_outlined,
|
controller.isConfirmPasswordVisible.value
|
||||||
color: AppColors.primary,
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off,
|
||||||
),
|
),
|
||||||
border: InputBorder.none,
|
onPressed: controller.toggleConfirmPasswordVisibility,
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
borderRadius: BorderRadius.circular(16),
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
|
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 _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() {
|
Widget _buildPhoneField() {
|
||||||
return Container(
|
return TextFormField(
|
||||||
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,
|
keyboardType: TextInputType.phone,
|
||||||
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Masukkan nomor HP anda',
|
hintText: 'Masukkan nomor HP anda',
|
||||||
hintStyle: TextStyle(color: AppColors.textLight),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
prefixIcon: Icon(Icons.phone_outlined, color: AppColors.primary),
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
border: InputBorder.none,
|
horizontal: 16,
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
vertical: 14,
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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 'package:get/get.dart';
|
||||||
import '../controllers/petugas_aset_controller.dart';
|
import '../controllers/petugas_aset_controller.dart';
|
||||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||||
|
import '../../../data/providers/aset_provider.dart';
|
||||||
|
|
||||||
class PetugasAsetBinding extends Bindings {
|
class PetugasAsetBinding extends Bindings {
|
||||||
@override
|
@override
|
||||||
@ -10,6 +11,7 @@ class PetugasAsetBinding extends Bindings {
|
|||||||
Get.put(PetugasBumdesDashboardController(), permanent: true);
|
Get.put(PetugasBumdesDashboardController(), permanent: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Get.lazyPut<AsetProvider>(() => AsetProvider());
|
||||||
Get.lazyPut<PetugasAsetController>(() => PetugasAsetController());
|
Get.lazyPut<PetugasAsetController>(() => PetugasAsetController());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../controllers/petugas_sewa_controller.dart';
|
import '../controllers/petugas_sewa_controller.dart';
|
||||||
|
import '../../../data/providers/aset_provider.dart';
|
||||||
|
|
||||||
class PetugasDetailSewaBinding extends Bindings {
|
class PetugasDetailSewaBinding extends Bindings {
|
||||||
@override
|
@override
|
||||||
void dependencies() {
|
void dependencies() {
|
||||||
|
// Ensure AsetProvider is registered
|
||||||
|
if (!Get.isRegistered<AsetProvider>()) {
|
||||||
|
Get.put(AsetProvider(), permanent: true);
|
||||||
|
}
|
||||||
// Memastikan controller sudah tersedia
|
// Memastikan controller sudah tersedia
|
||||||
Get.lazyPut<PetugasSewaController>(
|
Get.lazyPut<PetugasSewaController>(
|
||||||
() => PetugasSewaController(),
|
() => PetugasSewaController(),
|
||||||
|
|||||||
@ -1,15 +1,25 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||||
import '../controllers/petugas_paket_controller.dart';
|
import '../controllers/petugas_paket_controller.dart';
|
||||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||||
|
|
||||||
class PetugasPaketBinding extends Bindings {
|
class PetugasPaketBinding extends Bindings {
|
||||||
@override
|
@override
|
||||||
void dependencies() {
|
void dependencies() {
|
||||||
|
// Register AsetProvider first
|
||||||
|
if (!Get.isRegistered<AsetProvider>()) {
|
||||||
|
Get.put(AsetProvider(), permanent: true);
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure dashboard controller is registered
|
// Ensure dashboard controller is registered
|
||||||
if (!Get.isRegistered<PetugasBumdesDashboardController>()) {
|
if (!Get.isRegistered<PetugasBumdesDashboardController>()) {
|
||||||
Get.put(PetugasBumdesDashboardController(), permanent: true);
|
Get.put(PetugasBumdesDashboardController(), permanent: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Get.lazyPut<PetugasPaketController>(() => PetugasPaketController());
|
// Register the controller
|
||||||
|
Get.lazyPut<PetugasPaketController>(
|
||||||
|
() => PetugasPaketController(),
|
||||||
|
fenix: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../controllers/petugas_sewa_controller.dart';
|
import '../controllers/petugas_sewa_controller.dart';
|
||||||
|
import '../../../data/providers/aset_provider.dart';
|
||||||
|
|
||||||
class PetugasSewaBinding extends Bindings {
|
class PetugasSewaBinding extends Bindings {
|
||||||
@override
|
@override
|
||||||
void dependencies() {
|
void dependencies() {
|
||||||
|
// Ensure AsetProvider is registered
|
||||||
|
if (!Get.isRegistered<AsetProvider>()) {
|
||||||
|
Get.put(AsetProvider(), permanent: true);
|
||||||
|
}
|
||||||
Get.lazyPut<PetugasSewaController>(() => PetugasSewaController());
|
Get.lazyPut<PetugasSewaController>(() => PetugasSewaController());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import '../../../data/providers/aset_provider.dart';
|
||||||
|
import '../../../data/models/aset_model.dart';
|
||||||
|
|
||||||
class PetugasAsetController extends GetxController {
|
class PetugasAsetController extends GetxController {
|
||||||
|
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
||||||
// Observable lists for asset data
|
// Observable lists for asset data
|
||||||
final asetList = <Map<String, dynamic>>[].obs;
|
final asetList = <Map<String, dynamic>>[].obs;
|
||||||
final filteredAsetList = <Map<String, dynamic>>[].obs;
|
final filteredAsetList = <Map<String, dynamic>>[].obs;
|
||||||
@ -27,95 +32,100 @@ class PetugasAsetController extends GetxController {
|
|||||||
loadAsetData();
|
loadAsetData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load sample asset data (would be replaced with API call in production)
|
// Load asset data from AsetProvider
|
||||||
Future<void> loadAsetData() async {
|
Future<void> loadAsetData() async {
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Simulate API call with a delay
|
isLoading.value = true;
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
debugPrint('PetugasAsetController: Starting to load asset data...');
|
||||||
|
|
||||||
// Sample assets data
|
// Fetch data using AsetProvider
|
||||||
final sampleData = [
|
final asetData = await _asetProvider.getSewaAsets();
|
||||||
{
|
debugPrint(
|
||||||
'id': '1',
|
'PetugasAsetController: Fetched ${asetData.length} assets from Supabase',
|
||||||
'nama': 'Meja Rapat',
|
);
|
||||||
'kategori': 'Furniture',
|
|
||||||
'jenis': 'Sewa', // Added jenis field
|
|
||||||
'harga': 50000,
|
|
||||||
'satuan': 'per hari',
|
|
||||||
'stok': 10,
|
|
||||||
'deskripsi':
|
|
||||||
'Meja rapat kayu jati ukuran besar untuk acara pertemuan',
|
|
||||||
'gambar': 'https://example.com/meja.jpg',
|
|
||||||
'tersedia': true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '2',
|
|
||||||
'nama': 'Kursi Taman',
|
|
||||||
'kategori': 'Furniture',
|
|
||||||
'jenis': 'Sewa', // Added jenis field
|
|
||||||
'harga': 10000,
|
|
||||||
'satuan': 'per hari',
|
|
||||||
'stok': 50,
|
|
||||||
'deskripsi': 'Kursi taman plastik yang nyaman untuk acara outdoor',
|
|
||||||
'gambar': 'https://example.com/kursi.jpg',
|
|
||||||
'tersedia': true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '3',
|
|
||||||
'nama': 'Proyektor',
|
|
||||||
'kategori': 'Elektronik',
|
|
||||||
'jenis': 'Sewa', // Added jenis field
|
|
||||||
'harga': 100000,
|
|
||||||
'satuan': 'per hari',
|
|
||||||
'stok': 5,
|
|
||||||
'deskripsi': 'Proyektor HD dengan brightness tinggi',
|
|
||||||
'gambar': 'https://example.com/proyektor.jpg',
|
|
||||||
'tersedia': true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '4',
|
|
||||||
'nama': 'Sound System',
|
|
||||||
'kategori': 'Elektronik',
|
|
||||||
'jenis': 'Langganan', // Added jenis field
|
|
||||||
'harga': 200000,
|
|
||||||
'satuan': 'per bulan',
|
|
||||||
'stok': 3,
|
|
||||||
'deskripsi': 'Sound system lengkap dengan speaker dan mixer',
|
|
||||||
'gambar': 'https://example.com/sound.jpg',
|
|
||||||
'tersedia': false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '5',
|
|
||||||
'nama': 'Mobil Pick Up',
|
|
||||||
'kategori': 'Kendaraan',
|
|
||||||
'jenis': 'Langganan', // Added jenis field
|
|
||||||
'harga': 250000,
|
|
||||||
'satuan': 'per bulan',
|
|
||||||
'stok': 2,
|
|
||||||
'deskripsi': 'Mobil pick up untuk mengangkut barang',
|
|
||||||
'gambar': 'https://example.com/pickup.jpg',
|
|
||||||
'tersedia': true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '6',
|
|
||||||
'nama': 'Internet Fiber',
|
|
||||||
'kategori': 'Elektronik',
|
|
||||||
'jenis': 'Langganan', // Added jenis field
|
|
||||||
'harga': 350000,
|
|
||||||
'satuan': 'per bulan',
|
|
||||||
'stok': 15,
|
|
||||||
'deskripsi': 'Paket internet fiber 100Mbps untuk kantor',
|
|
||||||
'gambar': 'https://example.com/internet.jpg',
|
|
||||||
'tersedia': true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
asetList.assignAll(sampleData);
|
if (asetData.isEmpty) {
|
||||||
applyFilters(); // Apply default filters
|
debugPrint('PetugasAsetController: No assets found in Supabase');
|
||||||
} catch (e) {
|
}
|
||||||
print('Error loading asset data: $e');
|
|
||||||
|
final List<Map<String, dynamic>> mappedAsets = [];
|
||||||
|
int index = 0; // Initialize index counter
|
||||||
|
for (var aset in asetData) {
|
||||||
|
String displayKategori = 'Umum'; // Placeholder for descriptive category
|
||||||
|
// Attempt to derive a more specific category from description if needed, or add to AsetModel
|
||||||
|
if (aset.deskripsi.toLowerCase().contains('meja') ||
|
||||||
|
aset.deskripsi.toLowerCase().contains('kursi')) {
|
||||||
|
displayKategori = 'Furniture';
|
||||||
|
} else if (aset.deskripsi.toLowerCase().contains('proyektor') ||
|
||||||
|
aset.deskripsi.toLowerCase().contains('sound') ||
|
||||||
|
aset.deskripsi.toLowerCase().contains('internet')) {
|
||||||
|
displayKategori = 'Elektronik';
|
||||||
|
} else if (aset.deskripsi.toLowerCase().contains('mobil') ||
|
||||||
|
aset.deskripsi.toLowerCase().contains('kendaraan')) {
|
||||||
|
displayKategori = 'Kendaraan';
|
||||||
|
}
|
||||||
|
|
||||||
|
final map = {
|
||||||
|
'id': aset.id,
|
||||||
|
'nama': aset.nama,
|
||||||
|
'deskripsi': aset.deskripsi,
|
||||||
|
'harga':
|
||||||
|
aset.satuanWaktuSewa.isNotEmpty
|
||||||
|
? aset.satuanWaktuSewa.first['harga']
|
||||||
|
: 0,
|
||||||
|
'status': aset.status,
|
||||||
|
'kategori': displayKategori,
|
||||||
|
'jenis': aset.jenis ?? 'Sewa', // Add this line with default value
|
||||||
|
'imageUrl': aset.imageUrl ?? 'https://via.placeholder.com/150',
|
||||||
|
'satuan_waktu':
|
||||||
|
aset.satuanWaktuSewa.isNotEmpty
|
||||||
|
? aset.satuanWaktuSewa.first['nama_satuan_waktu'] ?? 'Hari'
|
||||||
|
: 'Hari',
|
||||||
|
'satuanWaktuSewa': aset.satuanWaktuSewa.toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
debugPrint('Mapped asset #$index: $map');
|
||||||
|
mappedAsets.add(map);
|
||||||
|
index++;
|
||||||
|
debugPrint('Deskripsi: ${aset.deskripsi}');
|
||||||
|
debugPrint('Kategori (from AsetModel): ${aset.kategori}');
|
||||||
|
debugPrint('Status: ${aset.status}');
|
||||||
|
debugPrint('Mapped Kategori for Petugas View: ${map['kategori']}');
|
||||||
|
debugPrint('Mapped Jenis for Petugas View: ${map['jenis']}');
|
||||||
|
debugPrint('--------------------------------');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate asetList with fetched data and apply filters
|
||||||
|
debugPrint(
|
||||||
|
'PetugasAsetController: Mapped ${mappedAsets.length} assets for display',
|
||||||
|
);
|
||||||
|
asetList.assignAll(mappedAsets); // Make data available to UI
|
||||||
|
debugPrint(
|
||||||
|
'PetugasAsetController: asetList now has ${asetList.length} items',
|
||||||
|
);
|
||||||
|
|
||||||
|
applyFilters(); // Apply initial filters
|
||||||
|
debugPrint(
|
||||||
|
'PetugasAsetController: Applied filters. filteredAsetList has ${filteredAsetList.length} items',
|
||||||
|
);
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
'PetugasAsetController: Data loading complete. Asset list populated and filters applied.',
|
||||||
|
);
|
||||||
|
debugPrint(
|
||||||
|
'PetugasAsetController: First asset name: ${mappedAsets.isNotEmpty ? mappedAsets[0]['nama'] : 'No assets'}',
|
||||||
|
);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
debugPrint('PetugasAsetController: Error loading asset data: $e');
|
||||||
|
debugPrint('PetugasAsetController: StackTrace: $stackTrace');
|
||||||
|
// Optionally, show a snackbar or error message to the user
|
||||||
|
Get.snackbar(
|
||||||
|
'Error Memuat Data',
|
||||||
|
'Gagal mengambil data aset dari server. Silakan coba lagi nanti.',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
@ -170,8 +180,10 @@ class PetugasAsetController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Change tab (Sewa or Langganan)
|
// Change tab (Sewa or Langganan)
|
||||||
void changeTab(int index) {
|
Future<void> changeTab(int index) async {
|
||||||
selectedTabIndex.value = index;
|
selectedTabIndex.value = index;
|
||||||
|
// Reload data when changing tabs to ensure we have the correct data for the selected tab
|
||||||
|
await loadAsetData();
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../../../data/providers/auth_provider.dart';
|
import '../../../data/providers/auth_provider.dart';
|
||||||
import '../../../routes/app_routes.dart';
|
import '../../../routes/app_routes.dart';
|
||||||
|
import '../../../services/sewa_service.dart';
|
||||||
|
import '../../../services/service_manager.dart';
|
||||||
|
import '../../../data/models/pembayaran_model.dart';
|
||||||
|
import '../../../services/pembayaran_service.dart';
|
||||||
|
|
||||||
class PetugasBumdesDashboardController extends GetxController {
|
class PetugasBumdesDashboardController extends GetxController {
|
||||||
AuthProvider? _authProvider;
|
AuthProvider? _authProvider;
|
||||||
@ -8,6 +12,8 @@ class PetugasBumdesDashboardController extends GetxController {
|
|||||||
// Reactive variables
|
// Reactive variables
|
||||||
final userEmail = ''.obs;
|
final userEmail = ''.obs;
|
||||||
final currentTabIndex = 0.obs;
|
final currentTabIndex = 0.obs;
|
||||||
|
final avatarUrl = ''.obs;
|
||||||
|
final userName = ''.obs;
|
||||||
|
|
||||||
// Revenue Statistics
|
// Revenue Statistics
|
||||||
final totalPendapatanBulanIni = 'Rp 8.500.000'.obs;
|
final totalPendapatanBulanIni = 'Rp 8.500.000'.obs;
|
||||||
@ -20,7 +26,7 @@ class PetugasBumdesDashboardController extends GetxController {
|
|||||||
final persentaseSewa = 100.obs;
|
final persentaseSewa = 100.obs;
|
||||||
|
|
||||||
// Revenue Trends (last 6 months)
|
// Revenue Trends (last 6 months)
|
||||||
final trendPendapatan = [4.2, 5.1, 4.8, 6.2, 7.2, 8.5].obs; // in millions
|
final trendPendapatan = <double>[].obs; // 6 bulan terakhir
|
||||||
|
|
||||||
// Status Counters for Sewa Aset
|
// Status Counters for Sewa Aset
|
||||||
final terlaksanaCount = 5.obs;
|
final terlaksanaCount = 5.obs;
|
||||||
@ -43,42 +49,128 @@ class PetugasBumdesDashboardController extends GetxController {
|
|||||||
final tagihanAktifCountSewa = 7.obs;
|
final tagihanAktifCountSewa = 7.obs;
|
||||||
final periksaPembayaranCountSewa = 2.obs;
|
final periksaPembayaranCountSewa = 2.obs;
|
||||||
|
|
||||||
|
// Statistik pendapatan
|
||||||
|
final totalPendapatan = 0.obs;
|
||||||
|
final pendapatanBulanIni = 0.obs;
|
||||||
|
final pendapatanBulanLalu = 0.obs;
|
||||||
|
final pendapatanTunai = 0.obs;
|
||||||
|
final pendapatanTransfer = 0.obs;
|
||||||
|
final trenPendapatan = <int>[].obs; // 6 bulan terakhir
|
||||||
|
|
||||||
|
// Dashboard statistics
|
||||||
|
final pembayaranStats = <String, dynamic>{}.obs;
|
||||||
|
final isStatsLoading = true.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
try {
|
try {
|
||||||
_authProvider = Get.find<AuthProvider>();
|
_authProvider = Get.find<AuthProvider>();
|
||||||
userEmail.value = _authProvider?.currentUser?.email ?? 'Tidak ada email';
|
userEmail.value = _authProvider?.currentUser?.email ?? 'Tidak ada email';
|
||||||
|
fetchPetugasAvatar();
|
||||||
|
fetchPetugasName();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error finding AuthProvider: $e');
|
print('Error finding AuthProvider: $e');
|
||||||
userEmail.value = 'Tidak ada email';
|
userEmail.value = 'Tidak ada email';
|
||||||
}
|
}
|
||||||
|
print('\u2705 PetugasBumdesDashboardController initialized successfully');
|
||||||
// In a real app, these counts would be fetched from backend
|
countSewaByStatus();
|
||||||
// loadStatusCounts();
|
fetchPembayaranStats();
|
||||||
print('✅ PetugasBumdesDashboardController initialized successfully');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to load status counts from backend
|
Future<void> countSewaByStatus() async {
|
||||||
// Future<void> loadStatusCounts() async {
|
try {
|
||||||
// try {
|
final data = await SewaService().fetchAllSewa();
|
||||||
// final response = await _asetProvider.getSewaStatusCounts();
|
menungguPembayaranCount.value =
|
||||||
// if (response != null) {
|
data.where((s) => s.status == 'MENUNGGU PEMBAYARAN').length;
|
||||||
// terlaksanaCount.value = response['terlaksana'] ?? 0;
|
periksaPembayaranCount.value =
|
||||||
// dijadwalkanCount.value = response['dijadwalkan'] ?? 0;
|
data.where((s) => s.status == 'PERIKSA PEMBAYARAN').length;
|
||||||
// aktifCount.value = response['aktif'] ?? 0;
|
diterimaCount.value = data.where((s) => s.status == 'DITERIMA').length;
|
||||||
// dibatalkanCount.value = response['dibatalkan'] ?? 0;
|
pembayaranDendaCount.value =
|
||||||
// menungguPembayaranCount.value = response['menunggu_pembayaran'] ?? 0;
|
data.where((s) => s.status == 'PEMBAYARAN DENDA').length;
|
||||||
// periksaPembayaranCount.value = response['periksa_pembayaran'] ?? 0;
|
periksaPembayaranDendaCount.value =
|
||||||
// diterimaCount.value = response['diterima'] ?? 0;
|
data.where((s) => s.status == 'PERIKSA PEMBAYARAN DENDA').length;
|
||||||
// pembayaranDendaCount.value = response['pembayaran_denda'] ?? 0;
|
selesaiCount.value = data.where((s) => s.status == 'SELESAI').length;
|
||||||
// periksaPembayaranDendaCount.value = response['periksa_pembayaran_denda'] ?? 0;
|
print(
|
||||||
// selesaiCount.value = response['selesai'] ?? 0;
|
'Count for MENUNGGU PEMBAYARAN: \\${menungguPembayaranCount.value}',
|
||||||
// }
|
);
|
||||||
// } catch (e) {
|
print('Count for PERIKSA PEMBAYARAN: \\${periksaPembayaranCount.value}');
|
||||||
// print('Error loading status counts: $e');
|
print('Count for DITERIMA: \\${diterimaCount.value}');
|
||||||
// }
|
print('Count for PEMBAYARAN DENDA: \\${pembayaranDendaCount.value}');
|
||||||
// }
|
print(
|
||||||
|
'Count for PERIKSA PEMBAYARAN DENDA: \\${periksaPembayaranDendaCount.value}',
|
||||||
|
);
|
||||||
|
print('Count for SELESAI: \\${selesaiCount.value}');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error counting sewa by status: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchPembayaranStats() async {
|
||||||
|
isStatsLoading.value = true;
|
||||||
|
try {
|
||||||
|
final stats = await PembayaranService().fetchStats();
|
||||||
|
pembayaranStats.value = stats;
|
||||||
|
// Set trendPendapatan from stats['trendPerMonth'] if available
|
||||||
|
if (stats['trendPerMonth'] != null) {
|
||||||
|
trendPendapatan.value = List<double>.from(stats['trendPerMonth']);
|
||||||
|
}
|
||||||
|
print('Pembayaran stats: $stats');
|
||||||
|
} catch (e, st) {
|
||||||
|
print('Error fetching pembayaran stats: $e\n$st');
|
||||||
|
pembayaranStats.value = {};
|
||||||
|
trendPendapatan.value = [];
|
||||||
|
}
|
||||||
|
isStatsLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchPetugasAvatar() async {
|
||||||
|
try {
|
||||||
|
final userId = _authProvider?.getCurrentUserId();
|
||||||
|
if (userId == null) return;
|
||||||
|
final client = _authProvider!.client;
|
||||||
|
final data =
|
||||||
|
await client
|
||||||
|
.from('petugas_bumdes')
|
||||||
|
.select('avatar')
|
||||||
|
.eq('id', userId)
|
||||||
|
.maybeSingle();
|
||||||
|
if (data != null &&
|
||||||
|
data['avatar'] != null &&
|
||||||
|
data['avatar'].toString().isNotEmpty) {
|
||||||
|
avatarUrl.value = data['avatar'].toString();
|
||||||
|
} else {
|
||||||
|
avatarUrl.value = '';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching petugas avatar: $e');
|
||||||
|
avatarUrl.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchPetugasName() async {
|
||||||
|
try {
|
||||||
|
final userId = _authProvider?.getCurrentUserId();
|
||||||
|
if (userId == null) return;
|
||||||
|
final client = _authProvider!.client;
|
||||||
|
final data =
|
||||||
|
await client
|
||||||
|
.from('petugas_bumdes')
|
||||||
|
.select('nama')
|
||||||
|
.eq('id', userId)
|
||||||
|
.maybeSingle();
|
||||||
|
if (data != null &&
|
||||||
|
data['nama'] != null &&
|
||||||
|
data['nama'].toString().isNotEmpty) {
|
||||||
|
userName.value = data['nama'].toString();
|
||||||
|
} else {
|
||||||
|
userName.value = '';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching petugas name: $e');
|
||||||
|
userName.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void changeTab(int index) {
|
void changeTab(int index) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart' show NumberFormat;
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
|
import 'package:bumrent_app/app/data/models/paket_model.dart';
|
||||||
|
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||||
|
|
||||||
class PetugasPaketController extends GetxController {
|
class PetugasPaketController extends GetxController {
|
||||||
final isLoading = false.obs;
|
// Dependencies
|
||||||
final searchQuery = ''.obs;
|
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
||||||
final selectedCategory = 'Semua'.obs;
|
|
||||||
final sortBy = 'Terbaru'.obs;
|
|
||||||
|
|
||||||
// Kategori untuk filter
|
// State
|
||||||
final categories = <String>[
|
final RxBool isLoading = false.obs;
|
||||||
'Semua',
|
final RxString searchQuery = ''.obs;
|
||||||
'Pesta',
|
final RxString selectedCategory = 'Semua'.obs;
|
||||||
'Rapat',
|
final RxString sortBy = 'Terbaru'.obs;
|
||||||
'Olahraga',
|
final RxList<PaketModel> packages = <PaketModel>[].obs;
|
||||||
'Pernikahan',
|
final RxList<PaketModel> filteredPackages = <PaketModel>[].obs;
|
||||||
'Lainnya',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Opsi pengurutan
|
// Sort options for the dropdown
|
||||||
final sortOptions = <String>[
|
final List<String> sortOptions = [
|
||||||
'Terbaru',
|
'Terbaru',
|
||||||
'Terlama',
|
'Terlama',
|
||||||
'Harga Tertinggi',
|
'Harga Tertinggi',
|
||||||
@ -27,172 +27,218 @@ class PetugasPaketController extends GetxController {
|
|||||||
'Nama Z-A',
|
'Nama Z-A',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Data dummy paket
|
// For backward compatibility
|
||||||
final paketList = <Map<String, dynamic>>[].obs;
|
final RxList<Map<String, dynamic>> paketList = <Map<String, dynamic>>[].obs;
|
||||||
final filteredPaketList = <Map<String, dynamic>>[].obs;
|
final RxList<Map<String, dynamic>> filteredPaketList = <Map<String, dynamic>>[].obs;
|
||||||
|
|
||||||
|
// Logger
|
||||||
|
late final Logger _logger;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
loadPaketData();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format harga ke Rupiah
|
// Initialize logger
|
||||||
String formatPrice(int price) {
|
_logger = Logger(
|
||||||
final formatter = NumberFormat.currency(
|
printer: PrettyPrinter(
|
||||||
locale: 'id',
|
methodCount: 0,
|
||||||
symbol: 'Rp ',
|
errorMethodCount: 5,
|
||||||
decimalDigits: 0,
|
colors: true,
|
||||||
|
printEmojis: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return formatter.format(price);
|
|
||||||
|
// Load initial data
|
||||||
|
fetchPackages();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load data paket dummy
|
/// Fetch packages from the API
|
||||||
Future<void> loadPaketData() async {
|
Future<void> fetchPackages() async {
|
||||||
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
await Future.delayed(const Duration(milliseconds: 800)); // Simulasi loading
|
_logger.i('🔄 [fetchPackages] Fetching packages...');
|
||||||
|
|
||||||
paketList.value = [
|
final result = await _asetProvider.getAllPaket();
|
||||||
{
|
|
||||||
'id': '1',
|
|
||||||
'nama': 'Paket Pesta Ulang Tahun',
|
|
||||||
'kategori': 'Pesta',
|
|
||||||
'harga': 500000,
|
|
||||||
'deskripsi':
|
|
||||||
'Paket lengkap untuk acara ulang tahun. Termasuk 5 meja, 20 kursi, backdrop, dan sound system.',
|
|
||||||
'tersedia': true,
|
|
||||||
'created_at': '2023-08-10',
|
|
||||||
'items': [
|
|
||||||
{'nama': 'Meja Panjang', 'jumlah': 5},
|
|
||||||
{'nama': 'Kursi Plastik', 'jumlah': 20},
|
|
||||||
{'nama': 'Sound System', 'jumlah': 1},
|
|
||||||
{'nama': 'Backdrop', 'jumlah': 1},
|
|
||||||
],
|
|
||||||
'gambar': 'https://example.com/images/paket_ultah.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '2',
|
|
||||||
'nama': 'Paket Rapat Sedang',
|
|
||||||
'kategori': 'Rapat',
|
|
||||||
'harga': 300000,
|
|
||||||
'deskripsi':
|
|
||||||
'Paket untuk rapat sedang. Termasuk 1 meja rapat besar, 10 kursi, proyektor, dan screen.',
|
|
||||||
'tersedia': true,
|
|
||||||
'created_at': '2023-09-05',
|
|
||||||
'items': [
|
|
||||||
{'nama': 'Meja Rapat', 'jumlah': 1},
|
|
||||||
{'nama': 'Kursi Kantor', 'jumlah': 10},
|
|
||||||
{'nama': 'Proyektor', 'jumlah': 1},
|
|
||||||
{'nama': 'Screen', 'jumlah': 1},
|
|
||||||
],
|
|
||||||
'gambar': 'https://example.com/images/paket_rapat.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '3',
|
|
||||||
'nama': 'Paket Pesta Pernikahan',
|
|
||||||
'kategori': 'Pernikahan',
|
|
||||||
'harga': 1500000,
|
|
||||||
'deskripsi':
|
|
||||||
'Paket lengkap untuk acara pernikahan. Termasuk 20 meja, 100 kursi, sound system, dekorasi, dan tenda.',
|
|
||||||
'tersedia': true,
|
|
||||||
'created_at': '2023-10-12',
|
|
||||||
'items': [
|
|
||||||
{'nama': 'Meja Bundar', 'jumlah': 20},
|
|
||||||
{'nama': 'Kursi Tamu', 'jumlah': 100},
|
|
||||||
{'nama': 'Sound System Besar', 'jumlah': 1},
|
|
||||||
{'nama': 'Tenda 10x10', 'jumlah': 2},
|
|
||||||
{'nama': 'Set Dekorasi Pengantin', 'jumlah': 1},
|
|
||||||
],
|
|
||||||
'gambar': 'https://example.com/images/paket_nikah.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '4',
|
|
||||||
'nama': 'Paket Olahraga Voli',
|
|
||||||
'kategori': 'Olahraga',
|
|
||||||
'harga': 200000,
|
|
||||||
'deskripsi':
|
|
||||||
'Paket perlengkapan untuk turnamen voli. Termasuk net, bola, dan tiang voli.',
|
|
||||||
'tersedia': false,
|
|
||||||
'created_at': '2023-07-22',
|
|
||||||
'items': [
|
|
||||||
{'nama': 'Net Voli', 'jumlah': 1},
|
|
||||||
{'nama': 'Bola Voli', 'jumlah': 3},
|
|
||||||
{'nama': 'Tiang Voli', 'jumlah': 2},
|
|
||||||
],
|
|
||||||
'gambar': 'https://example.com/images/paket_voli.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '5',
|
|
||||||
'nama': 'Paket Pesta Anak',
|
|
||||||
'kategori': 'Pesta',
|
|
||||||
'harga': 350000,
|
|
||||||
'deskripsi':
|
|
||||||
'Paket untuk pesta ulang tahun anak-anak. Termasuk 3 meja, 15 kursi, dekorasi tema, dan sound system kecil.',
|
|
||||||
'tersedia': true,
|
|
||||||
'created_at': '2023-11-01',
|
|
||||||
'items': [
|
|
||||||
{'nama': 'Meja Anak', 'jumlah': 3},
|
|
||||||
{'nama': 'Kursi Anak', 'jumlah': 15},
|
|
||||||
{'nama': 'Set Dekorasi Tema', 'jumlah': 1},
|
|
||||||
{'nama': 'Sound System Kecil', 'jumlah': 1},
|
|
||||||
],
|
|
||||||
'gambar': 'https://example.com/images/paket_anak.jpg',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
filterPaket();
|
if (result.isEmpty) {
|
||||||
|
_logger.w('ℹ️ [fetchPackages] No packages found');
|
||||||
|
packages.clear();
|
||||||
|
filteredPackages.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
packages.assignAll(result);
|
||||||
|
filteredPackages.assignAll(result);
|
||||||
|
|
||||||
|
// Update legacy list for backward compatibility
|
||||||
|
_updateLegacyPaketList();
|
||||||
|
|
||||||
|
_logger.i('✅ [fetchPackages] Successfully loaded ${result.length} packages');
|
||||||
|
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_logger.e('❌ [fetchPackages] Error fetching packages',
|
||||||
|
error: e,
|
||||||
|
stackTrace: stackTrace);
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Gagal memuat data paket. Silakan coba lagi.',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter paket berdasarkan search query dan kategori
|
|
||||||
void filterPaket() {
|
|
||||||
filteredPaketList.value =
|
|
||||||
paketList.where((paket) {
|
|
||||||
final matchesQuery =
|
|
||||||
paket['nama'].toString().toLowerCase().contains(
|
|
||||||
searchQuery.value.toLowerCase(),
|
|
||||||
) ||
|
|
||||||
paket['deskripsi'].toString().toLowerCase().contains(
|
|
||||||
searchQuery.value.toLowerCase(),
|
|
||||||
);
|
|
||||||
|
|
||||||
final matchesCategory =
|
|
||||||
selectedCategory.value == 'Semua' ||
|
|
||||||
paket['kategori'] == selectedCategory.value;
|
|
||||||
|
|
||||||
return matchesQuery && matchesCategory;
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
// Sort the filtered list
|
|
||||||
sortFilteredList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the filtered list
|
/// Update legacy paketList for backward compatibility
|
||||||
|
void _updateLegacyPaketList() {
|
||||||
|
try {
|
||||||
|
_logger.d('🔄 [_updateLegacyPaketList] Updating legacy paketList...');
|
||||||
|
|
||||||
|
final List<Map<String, dynamic>> legacyList = packages.map((pkg) {
|
||||||
|
return {
|
||||||
|
'id': pkg.id,
|
||||||
|
'nama': pkg.nama,
|
||||||
|
'deskripsi': pkg.deskripsi,
|
||||||
|
'harga': pkg.harga,
|
||||||
|
'kuantitas': pkg.kuantitas,
|
||||||
|
'status': pkg.status, // Add status to legacy mapping
|
||||||
|
'foto': pkg.foto,
|
||||||
|
'foto_paket': pkg.foto_paket,
|
||||||
|
'images': pkg.images,
|
||||||
|
'satuanWaktuSewa': pkg.satuanWaktuSewa,
|
||||||
|
'created_at': pkg.createdAt,
|
||||||
|
'updated_at': pkg.updatedAt,
|
||||||
|
};
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
paketList.assignAll(legacyList);
|
||||||
|
filteredPaketList.assignAll(legacyList);
|
||||||
|
|
||||||
|
_logger.d('✅ [_updateLegacyPaketList] Updated ${legacyList.length} packages');
|
||||||
|
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_logger.e('❌ [_updateLegacyPaketList] Error updating legacy list',
|
||||||
|
error: e,
|
||||||
|
stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For backward compatibility
|
||||||
|
Future<void> loadPaketData() async {
|
||||||
|
_logger.d('ℹ️ [loadPaketData] Using fetchPackages() instead');
|
||||||
|
await fetchPackages();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter packages based on search query and category
|
||||||
|
void filterPaket() {
|
||||||
|
try {
|
||||||
|
_logger.d('🔄 [filterPaket] Filtering packages...');
|
||||||
|
|
||||||
|
if (searchQuery.value.isEmpty && selectedCategory.value == 'Semua') {
|
||||||
|
filteredPackages.value = List.from(packages);
|
||||||
|
filteredPaketList.value = List.from(paketList);
|
||||||
|
} else {
|
||||||
|
// Filter new packages
|
||||||
|
filteredPackages.value = packages.where((paket) {
|
||||||
|
final matchesSearch = searchQuery.value.isEmpty ||
|
||||||
|
paket.nama.toLowerCase().contains(searchQuery.value.toLowerCase());
|
||||||
|
|
||||||
|
// For now, we're not using categories in the new model
|
||||||
|
// You can add category filtering if needed
|
||||||
|
final matchesCategory = selectedCategory.value == 'Semua';
|
||||||
|
|
||||||
|
return matchesSearch && matchesCategory;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Also update legacy list for backward compatibility
|
||||||
|
filteredPaketList.value = paketList.where((paket) {
|
||||||
|
final matchesSearch = searchQuery.value.isEmpty ||
|
||||||
|
(paket['nama']?.toString() ?? '').toLowerCase()
|
||||||
|
.contains(searchQuery.value.toLowerCase());
|
||||||
|
|
||||||
|
// For legacy support, check if category exists
|
||||||
|
final matchesCategory = selectedCategory.value == 'Semua' ||
|
||||||
|
(paket['kategori']?.toString() ?? '') == selectedCategory.value;
|
||||||
|
|
||||||
|
return matchesSearch && matchesCategory;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
sortFilteredList();
|
||||||
|
_logger.d('✅ [filterPaket] Filtered to ${filteredPackages.length} packages');
|
||||||
|
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_logger.e('❌ [filterPaket] Error filtering packages',
|
||||||
|
error: e,
|
||||||
|
stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sort the filtered list based on the selected sort option
|
||||||
void sortFilteredList() {
|
void sortFilteredList() {
|
||||||
|
try {
|
||||||
|
_logger.d('🔄 [sortFilteredList] Sorting packages by ${sortBy.value}');
|
||||||
|
|
||||||
|
// Sort new packages
|
||||||
switch (sortBy.value) {
|
switch (sortBy.value) {
|
||||||
case 'Terbaru':
|
case 'Terbaru':
|
||||||
filteredPaketList.sort(
|
filteredPackages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
(a, b) => b['created_at'].compareTo(a['created_at']),
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 'Terlama':
|
case 'Terlama':
|
||||||
filteredPaketList.sort(
|
filteredPackages.sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||||
(a, b) => a['created_at'].compareTo(b['created_at']),
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 'Harga Tertinggi':
|
case 'Harga Tertinggi':
|
||||||
filteredPaketList.sort((a, b) => b['harga'].compareTo(a['harga']));
|
filteredPackages.sort((a, b) => b.harga.compareTo(a.harga));
|
||||||
break;
|
break;
|
||||||
case 'Harga Terendah':
|
case 'Harga Terendah':
|
||||||
filteredPaketList.sort((a, b) => a['harga'].compareTo(b['harga']));
|
filteredPackages.sort((a, b) => a.harga.compareTo(b.harga));
|
||||||
break;
|
break;
|
||||||
case 'Nama A-Z':
|
case 'Nama A-Z':
|
||||||
filteredPaketList.sort((a, b) => a['nama'].compareTo(b['nama']));
|
filteredPackages.sort((a, b) => a.nama.compareTo(b.nama));
|
||||||
break;
|
break;
|
||||||
case 'Nama Z-A':
|
case 'Nama Z-A':
|
||||||
filteredPaketList.sort((a, b) => b['nama'].compareTo(a['nama']));
|
filteredPackages.sort((a, b) => b.nama.compareTo(a.nama));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also sort legacy list for backward compatibility
|
||||||
|
switch (sortBy.value) {
|
||||||
|
case 'Terbaru':
|
||||||
|
filteredPaketList.sort((a, b) =>
|
||||||
|
((b['created_at'] ?? '') as String).compareTo((a['created_at'] ?? '') as String));
|
||||||
|
break;
|
||||||
|
case 'Terlama':
|
||||||
|
filteredPaketList.sort((a, b) =>
|
||||||
|
((a['created_at'] ?? '') as String).compareTo((b['created_at'] ?? '') as String));
|
||||||
|
break;
|
||||||
|
case 'Harga Tertinggi':
|
||||||
|
filteredPaketList.sort((a, b) =>
|
||||||
|
((b['harga'] ?? 0) as int).compareTo((a['harga'] ?? 0) as int));
|
||||||
|
break;
|
||||||
|
case 'Harga Terendah':
|
||||||
|
filteredPaketList.sort((a, b) =>
|
||||||
|
((a['harga'] ?? 0) as int).compareTo((b['harga'] ?? 0) as int));
|
||||||
|
break;
|
||||||
|
case 'Nama A-Z':
|
||||||
|
filteredPaketList.sort((a, b) =>
|
||||||
|
((a['nama'] ?? '') as String).compareTo((b['nama'] ?? '') as String));
|
||||||
|
break;
|
||||||
|
case 'Nama Z-A':
|
||||||
|
filteredPaketList.sort((a, b) =>
|
||||||
|
((b['nama'] ?? '') as String).compareTo((a['nama'] ?? '') as String));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.d('✅ [sortFilteredList] Sorted ${filteredPackages.length} packages');
|
||||||
|
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_logger.e('❌ [sortFilteredList] Error sorting packages',
|
||||||
|
error: e,
|
||||||
|
stackTrace: stackTrace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set search query dan filter paket
|
// Set search query dan filter paket
|
||||||
@ -214,40 +260,134 @@ class PetugasPaketController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tambah paket baru
|
// Tambah paket baru
|
||||||
void addPaket(Map<String, dynamic> paket) {
|
Future<void> addPaket(Map<String, dynamic> paketData) async {
|
||||||
paketList.add(paket);
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
// Convert to PaketModel
|
||||||
|
final newPaket = PaketModel.fromJson({
|
||||||
|
...paketData,
|
||||||
|
'id': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
|
'updated_at': DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add to the list
|
||||||
|
packages.add(newPaket);
|
||||||
|
_updateLegacyPaketList();
|
||||||
filterPaket();
|
filterPaket();
|
||||||
|
|
||||||
Get.back();
|
Get.back();
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Sukses',
|
'Sukses',
|
||||||
'Paket baru berhasil ditambahkan',
|
'Paket baru berhasil ditambahkan',
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
snackPosition: SnackPosition.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
|
// Edit paket
|
||||||
void editPaket(String id, Map<String, dynamic> updatedPaket) {
|
Future<void> editPaket(String id, Map<String, dynamic> updatedData) async {
|
||||||
final index = paketList.indexWhere((element) => element['id'] == id);
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
final index = packages.indexWhere((pkg) => pkg.id == id);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
paketList[index] = updatedPaket;
|
// Update the package
|
||||||
|
final updatedPaket = packages[index].copyWith(
|
||||||
|
nama: updatedData['nama']?.toString() ?? packages[index].nama,
|
||||||
|
deskripsi: updatedData['deskripsi']?.toString() ?? packages[index].deskripsi,
|
||||||
|
kuantitas: (updatedData['kuantitas'] is int)
|
||||||
|
? updatedData['kuantitas']
|
||||||
|
: (int.tryParse(updatedData['kuantitas']?.toString() ?? '0') ?? packages[index].kuantitas),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
packages[index] = updatedPaket;
|
||||||
|
_updateLegacyPaketList();
|
||||||
filterPaket();
|
filterPaket();
|
||||||
|
|
||||||
Get.back();
|
Get.back();
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Sukses',
|
'Sukses',
|
||||||
'Paket berhasil diperbarui',
|
'Paket berhasil diperbarui',
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
colorText: Colors.white,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_logger.e('❌ [editPaket] Error updating package',
|
||||||
|
error: e,
|
||||||
|
stackTrace: stackTrace);
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Gagal memperbarui paket. Silakan coba lagi.',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hapus paket
|
// Hapus paket
|
||||||
void deletePaket(String id) {
|
Future<void> deletePaket(String id) async {
|
||||||
paketList.removeWhere((element) => element['id'] == id);
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
// Remove from the main list
|
||||||
|
packages.removeWhere((pkg) => pkg.id == id);
|
||||||
|
_updateLegacyPaketList();
|
||||||
filterPaket();
|
filterPaket();
|
||||||
|
|
||||||
|
Get.back();
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Sukses',
|
'Sukses',
|
||||||
'Paket berhasil dihapus',
|
'Paket berhasil dihapus',
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
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:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import '../../../services/sewa_service.dart';
|
||||||
|
import '../../../data/models/rental_booking_model.dart';
|
||||||
|
import '../../../data/providers/aset_provider.dart';
|
||||||
|
|
||||||
class PetugasSewaController extends GetxController {
|
class PetugasSewaController extends GetxController {
|
||||||
// Reactive variables
|
// Reactive variables
|
||||||
@ -7,7 +10,7 @@ class PetugasSewaController extends GetxController {
|
|||||||
final searchQuery = ''.obs;
|
final searchQuery = ''.obs;
|
||||||
final orderIdQuery = ''.obs;
|
final orderIdQuery = ''.obs;
|
||||||
final selectedStatusFilter = 'Semua'.obs;
|
final selectedStatusFilter = 'Semua'.obs;
|
||||||
final filteredSewaList = <Map<String, dynamic>>[].obs;
|
final filteredSewaList = <SewaModel>[].obs;
|
||||||
|
|
||||||
// Filter options
|
// Filter options
|
||||||
final List<String> statusFilters = [
|
final List<String> statusFilters = [
|
||||||
@ -15,13 +18,19 @@ class PetugasSewaController extends GetxController {
|
|||||||
'Menunggu Pembayaran',
|
'Menunggu Pembayaran',
|
||||||
'Periksa Pembayaran',
|
'Periksa Pembayaran',
|
||||||
'Diterima',
|
'Diterima',
|
||||||
|
'Aktif',
|
||||||
'Dikembalikan',
|
'Dikembalikan',
|
||||||
'Selesai',
|
'Selesai',
|
||||||
'Dibatalkan',
|
'Dibatalkan',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Mock data for sewa list
|
// Mock data for sewa list
|
||||||
final RxList<Map<String, dynamic>> sewaList = <Map<String, dynamic>>[].obs;
|
final RxList<SewaModel> sewaList = <SewaModel>[].obs;
|
||||||
|
|
||||||
|
// Payment option state (per sewa)
|
||||||
|
final Map<String, RxBool> isFullPaymentMap = {};
|
||||||
|
final Map<String, TextEditingController> nominalControllerMap = {};
|
||||||
|
final Map<String, RxString> paymentMethodMap = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -41,25 +50,21 @@ class PetugasSewaController extends GetxController {
|
|||||||
void _updateFilteredList() {
|
void _updateFilteredList() {
|
||||||
filteredSewaList.value =
|
filteredSewaList.value =
|
||||||
sewaList.where((sewa) {
|
sewaList.where((sewa) {
|
||||||
// Apply search filter
|
final query = searchQuery.value.toLowerCase();
|
||||||
final matchesSearch = sewa['nama_warga']
|
// Apply search filter: nama warga, id pesanan, atau asetId
|
||||||
.toString()
|
final matchesSearch =
|
||||||
.toLowerCase()
|
sewa.wargaNama.toLowerCase().contains(query) ||
|
||||||
.contains(searchQuery.value.toLowerCase());
|
sewa.id.toLowerCase().contains(query) ||
|
||||||
|
(sewa.asetId != null &&
|
||||||
// Apply order ID filter if provided
|
sewa.asetId!.toLowerCase().contains(query));
|
||||||
final matchesOrderId =
|
|
||||||
orderIdQuery.value.isEmpty ||
|
|
||||||
sewa['order_id'].toString().toLowerCase().contains(
|
|
||||||
orderIdQuery.value.toLowerCase(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Apply status filter if not 'Semua'
|
// Apply status filter if not 'Semua'
|
||||||
final matchesStatus =
|
final matchesStatus =
|
||||||
selectedStatusFilter.value == 'Semua' ||
|
selectedStatusFilter.value == 'Semua' ||
|
||||||
sewa['status'] == selectedStatusFilter.value;
|
sewa.status.toUpperCase() ==
|
||||||
|
selectedStatusFilter.value.toUpperCase();
|
||||||
|
|
||||||
return matchesSearch && matchesOrderId && matchesStatus;
|
return matchesSearch && matchesStatus;
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,100 +73,8 @@ class PetugasSewaController extends GetxController {
|
|||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Simulate API call delay
|
final data = await SewaService().fetchAllSewa();
|
||||||
await Future.delayed(const Duration(milliseconds: 800));
|
sewaList.assignAll(data);
|
||||||
|
|
||||||
// Populate with mock data
|
|
||||||
sewaList.assignAll([
|
|
||||||
{
|
|
||||||
'id': '1',
|
|
||||||
'order_id': 'SWA-001',
|
|
||||||
'nama_warga': 'Sukimin',
|
|
||||||
'nama_aset': 'Mobil Pickup',
|
|
||||||
'tanggal_mulai': '2025-02-05',
|
|
||||||
'tanggal_selesai': '2025-02-10',
|
|
||||||
'total_biaya': 45000,
|
|
||||||
'status': 'Diterima',
|
|
||||||
'photo_url': 'https://example.com/photo1.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '2',
|
|
||||||
'order_id': 'SWA-002',
|
|
||||||
'nama_warga': 'Sukimin',
|
|
||||||
'nama_aset': 'Mobil Pickup',
|
|
||||||
'tanggal_mulai': '2025-02-15',
|
|
||||||
'tanggal_selesai': '2025-02-20',
|
|
||||||
'total_biaya': 30000,
|
|
||||||
'status': 'Selesai',
|
|
||||||
'photo_url': 'https://example.com/photo2.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '3',
|
|
||||||
'order_id': 'SWA-003',
|
|
||||||
'nama_warga': 'Sukimin',
|
|
||||||
'nama_aset': 'Mobil Pickup',
|
|
||||||
'tanggal_mulai': '2025-02-25',
|
|
||||||
'tanggal_selesai': '2025-03-01',
|
|
||||||
'total_biaya': 35000,
|
|
||||||
'status': 'Menunggu Pembayaran',
|
|
||||||
'photo_url': 'https://example.com/photo3.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '4',
|
|
||||||
'order_id': 'SWA-004',
|
|
||||||
'nama_warga': 'Sukimin',
|
|
||||||
'nama_aset': 'Mobil Pickup',
|
|
||||||
'tanggal_mulai': '2025-03-05',
|
|
||||||
'tanggal_selesai': '2025-03-08',
|
|
||||||
'total_biaya': 20000,
|
|
||||||
'status': 'Periksa Pembayaran',
|
|
||||||
'photo_url': 'https://example.com/photo4.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '5',
|
|
||||||
'order_id': 'SWA-005',
|
|
||||||
'nama_warga': 'Sukimin',
|
|
||||||
'nama_aset': 'Mobil Pickup',
|
|
||||||
'tanggal_mulai': '2025-03-12',
|
|
||||||
'tanggal_selesai': '2025-03-14',
|
|
||||||
'total_biaya': 15000,
|
|
||||||
'status': 'Dibatalkan',
|
|
||||||
'photo_url': 'https://example.com/photo5.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '6',
|
|
||||||
'order_id': 'SWA-006',
|
|
||||||
'nama_warga': 'Sukimin',
|
|
||||||
'nama_aset': 'Mobil Pickup',
|
|
||||||
'tanggal_mulai': '2025-03-18',
|
|
||||||
'tanggal_selesai': '2025-03-20',
|
|
||||||
'total_biaya': 25000,
|
|
||||||
'status': 'Pembayaran Denda',
|
|
||||||
'photo_url': 'https://example.com/photo6.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '7',
|
|
||||||
'order_id': 'SWA-007',
|
|
||||||
'nama_warga': 'Sukimin',
|
|
||||||
'nama_aset': 'Mobil Pickup',
|
|
||||||
'tanggal_mulai': '2025-03-25',
|
|
||||||
'tanggal_selesai': '2025-03-28',
|
|
||||||
'total_biaya': 40000,
|
|
||||||
'status': 'Periksa Denda',
|
|
||||||
'photo_url': 'https://example.com/photo7.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '8',
|
|
||||||
'order_id': 'SWA-008',
|
|
||||||
'nama_warga': 'Sukimin',
|
|
||||||
'nama_aset': 'Mobil Pickup',
|
|
||||||
'tanggal_mulai': '2025-04-02',
|
|
||||||
'tanggal_selesai': '2025-04-05',
|
|
||||||
'total_biaya': 10000,
|
|
||||||
'status': 'Dikembalikan',
|
|
||||||
'photo_url': 'https://example.com/photo8.jpg',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error loading sewa data: $e');
|
print('Error loading sewa data: $e');
|
||||||
} finally {
|
} finally {
|
||||||
@ -196,10 +109,11 @@ class PetugasSewaController extends GetxController {
|
|||||||
sewaList.where((sewa) {
|
sewaList.where((sewa) {
|
||||||
bool matchesStatus =
|
bool matchesStatus =
|
||||||
selectedStatusFilter.value == 'Semua' ||
|
selectedStatusFilter.value == 'Semua' ||
|
||||||
sewa['status'] == selectedStatusFilter.value;
|
sewa.status.toUpperCase() ==
|
||||||
|
selectedStatusFilter.value.toUpperCase();
|
||||||
bool matchesSearch =
|
bool matchesSearch =
|
||||||
searchQuery.value.isEmpty ||
|
searchQuery.value.isEmpty ||
|
||||||
sewa['nama_warga'].toLowerCase().contains(
|
sewa.wargaNama.toLowerCase().contains(
|
||||||
searchQuery.value.toLowerCase(),
|
searchQuery.value.toLowerCase(),
|
||||||
);
|
);
|
||||||
return matchesStatus && matchesSearch;
|
return matchesStatus && matchesSearch;
|
||||||
@ -213,102 +127,367 @@ class PetugasSewaController extends GetxController {
|
|||||||
|
|
||||||
// Get color based on status
|
// Get color based on status
|
||||||
Color getStatusColor(String status) {
|
Color getStatusColor(String status) {
|
||||||
switch (status) {
|
switch (status.toUpperCase()) {
|
||||||
case 'Menunggu Pembayaran':
|
case 'MENUNGGU PEMBAYARAN':
|
||||||
return Colors.orange;
|
return Colors.orangeAccent;
|
||||||
case 'Periksa Pembayaran':
|
case 'PERIKSA PEMBAYARAN':
|
||||||
return Colors.amber.shade700;
|
return Colors.amber;
|
||||||
case 'Diterima':
|
case 'DITERIMA':
|
||||||
return Colors.blue;
|
return Colors.blueAccent;
|
||||||
case 'Pembayaran Denda':
|
case 'AKTIF':
|
||||||
return Colors.deepOrange;
|
|
||||||
case 'Periksa Denda':
|
|
||||||
return Colors.red.shade600;
|
|
||||||
case 'Dikembalikan':
|
|
||||||
return Colors.teal;
|
|
||||||
case 'Sedang Disewa':
|
|
||||||
return Colors.green;
|
return Colors.green;
|
||||||
case 'Selesai':
|
case 'PEMBAYARAN DENDA':
|
||||||
|
return Colors.deepOrangeAccent;
|
||||||
|
case 'PERIKSA PEMBAYARAN DENDA':
|
||||||
|
return Colors.redAccent;
|
||||||
|
case 'DIKEMBALIKAN':
|
||||||
|
return Colors.teal;
|
||||||
|
case 'SELESAI':
|
||||||
return Colors.purple;
|
return Colors.purple;
|
||||||
case 'Dibatalkan':
|
case 'DIBATALKAN':
|
||||||
return Colors.red;
|
return Colors.red;
|
||||||
default:
|
default:
|
||||||
return Colors.grey;
|
return Colors.grey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle sewa approval (from "Periksa Pembayaran" to "Diterima")
|
// Get icon based on status
|
||||||
void approveSewa(String id) {
|
IconData getStatusIcon(String status) {
|
||||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
switch (status) {
|
||||||
if (index != -1) {
|
case 'MENUNGGU PEMBAYARAN':
|
||||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
return Icons.payments_outlined;
|
||||||
final currentStatus = sewa['status'];
|
case 'PERIKSA PEMBAYARAN':
|
||||||
|
return Icons.fact_check_outlined;
|
||||||
if (currentStatus == 'Periksa Pembayaran') {
|
case 'DITERIMA':
|
||||||
sewa['status'] = 'Diterima';
|
return Icons.check_circle_outlined;
|
||||||
} else if (currentStatus == 'Periksa Denda') {
|
case 'AKTIF':
|
||||||
sewa['status'] = 'Selesai';
|
return Icons.play_circle_outline;
|
||||||
} else if (currentStatus == 'Menunggu Pembayaran') {
|
case 'PEMBYARAN DENDA':
|
||||||
sewa['status'] = 'Periksa Pembayaran';
|
return Icons.money_off_csred_outlined;
|
||||||
|
case 'PERIKSA PEMBAYARAN DENDA':
|
||||||
|
return Icons.assignment_late_outlined;
|
||||||
|
case 'DIKEMBALIKAN':
|
||||||
|
return Icons.assignment_return_outlined;
|
||||||
|
case 'SELESAI':
|
||||||
|
return Icons.task_alt_outlined;
|
||||||
|
case 'DIBATALKAN':
|
||||||
|
return Icons.cancel_outlined;
|
||||||
|
default:
|
||||||
|
return Icons.help_outline_rounded;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sewaList[index] = sewa;
|
// Handle sewa approval (from "Periksa Pembayaran" to "Diterima")
|
||||||
|
void approveSewa(String id) {
|
||||||
|
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||||
|
if (index != -1) {
|
||||||
|
final sewa = sewaList[index];
|
||||||
|
final currentStatus = sewa.status;
|
||||||
|
String? newStatus;
|
||||||
|
if (currentStatus == 'PERIKSA PEMBAYARAN') {
|
||||||
|
newStatus = 'DITERIMA';
|
||||||
|
} else if (currentStatus == 'PERIKSA PEMBAYARAN DENDA') {
|
||||||
|
newStatus = 'SELESAI';
|
||||||
|
} else if (currentStatus == 'MENUNGGU PEMBAYARAN') {
|
||||||
|
newStatus = 'PERIKSA PEMBAYARAN';
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
sewaList[index] = SewaModel(
|
||||||
|
id: sewa.id,
|
||||||
|
userId: sewa.userId,
|
||||||
|
status: newStatus,
|
||||||
|
waktuMulai: sewa.waktuMulai,
|
||||||
|
waktuSelesai: sewa.waktuSelesai,
|
||||||
|
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||||
|
tipePesanan: sewa.tipePesanan,
|
||||||
|
kuantitas: sewa.kuantitas,
|
||||||
|
asetId: sewa.asetId,
|
||||||
|
asetNama: sewa.asetNama,
|
||||||
|
asetFoto: sewa.asetFoto,
|
||||||
|
paketId: sewa.paketId,
|
||||||
|
paketNama: sewa.paketNama,
|
||||||
|
paketFoto: sewa.paketFoto,
|
||||||
|
totalTagihan: sewa.totalTagihan,
|
||||||
|
wargaNama: sewa.wargaNama,
|
||||||
|
wargaNoHp: sewa.wargaNoHp,
|
||||||
|
wargaAvatar: sewa.wargaAvatar,
|
||||||
|
);
|
||||||
sewaList.refresh();
|
sewaList.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle sewa rejection or cancellation
|
// Handle sewa rejection or cancellation
|
||||||
void rejectSewa(String id) {
|
void rejectSewa(String id) {
|
||||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
final sewa = sewaList[index];
|
||||||
sewa['status'] = 'Dibatalkan';
|
sewaList[index] = SewaModel(
|
||||||
sewaList[index] = sewa;
|
id: sewa.id,
|
||||||
|
userId: sewa.userId,
|
||||||
|
status: 'Dibatalkan',
|
||||||
|
waktuMulai: sewa.waktuMulai,
|
||||||
|
waktuSelesai: sewa.waktuSelesai,
|
||||||
|
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||||
|
tipePesanan: sewa.tipePesanan,
|
||||||
|
kuantitas: sewa.kuantitas,
|
||||||
|
asetId: sewa.asetId,
|
||||||
|
asetNama: sewa.asetNama,
|
||||||
|
asetFoto: sewa.asetFoto,
|
||||||
|
paketId: sewa.paketId,
|
||||||
|
paketNama: sewa.paketNama,
|
||||||
|
paketFoto: sewa.paketFoto,
|
||||||
|
totalTagihan: sewa.totalTagihan,
|
||||||
|
wargaNama: sewa.wargaNama,
|
||||||
|
wargaNoHp: sewa.wargaNoHp,
|
||||||
|
wargaAvatar: sewa.wargaAvatar,
|
||||||
|
);
|
||||||
sewaList.refresh();
|
sewaList.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request payment for penalty
|
// Request payment for penalty
|
||||||
void requestPenaltyPayment(String id) {
|
void requestPenaltyPayment(String id) {
|
||||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
final sewa = sewaList[index];
|
||||||
sewa['status'] = 'Pembayaran Denda';
|
sewaList[index] = SewaModel(
|
||||||
sewaList[index] = sewa;
|
id: sewa.id,
|
||||||
|
userId: sewa.userId,
|
||||||
|
status: 'Pembayaran Denda',
|
||||||
|
waktuMulai: sewa.waktuMulai,
|
||||||
|
waktuSelesai: sewa.waktuSelesai,
|
||||||
|
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||||
|
tipePesanan: sewa.tipePesanan,
|
||||||
|
kuantitas: sewa.kuantitas,
|
||||||
|
asetId: sewa.asetId,
|
||||||
|
asetNama: sewa.asetNama,
|
||||||
|
asetFoto: sewa.asetFoto,
|
||||||
|
paketId: sewa.paketId,
|
||||||
|
paketNama: sewa.paketNama,
|
||||||
|
paketFoto: sewa.paketFoto,
|
||||||
|
totalTagihan: sewa.totalTagihan,
|
||||||
|
wargaNama: sewa.wargaNama,
|
||||||
|
wargaNoHp: sewa.wargaNoHp,
|
||||||
|
wargaAvatar: sewa.wargaAvatar,
|
||||||
|
);
|
||||||
sewaList.refresh();
|
sewaList.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark penalty payment as requiring inspection
|
// Mark penalty payment as requiring inspection
|
||||||
void markPenaltyForInspection(String id) {
|
void markPenaltyForInspection(String id) {
|
||||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
final sewa = sewaList[index];
|
||||||
sewa['status'] = 'Periksa Denda';
|
sewaList[index] = SewaModel(
|
||||||
sewaList[index] = sewa;
|
id: sewa.id,
|
||||||
|
userId: sewa.userId,
|
||||||
|
status: 'Periksa Denda',
|
||||||
|
waktuMulai: sewa.waktuMulai,
|
||||||
|
waktuSelesai: sewa.waktuSelesai,
|
||||||
|
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||||
|
tipePesanan: sewa.tipePesanan,
|
||||||
|
kuantitas: sewa.kuantitas,
|
||||||
|
asetId: sewa.asetId,
|
||||||
|
asetNama: sewa.asetNama,
|
||||||
|
asetFoto: sewa.asetFoto,
|
||||||
|
paketId: sewa.paketId,
|
||||||
|
paketNama: sewa.paketNama,
|
||||||
|
paketFoto: sewa.paketFoto,
|
||||||
|
totalTagihan: sewa.totalTagihan,
|
||||||
|
wargaNama: sewa.wargaNama,
|
||||||
|
wargaNoHp: sewa.wargaNoHp,
|
||||||
|
wargaAvatar: sewa.wargaAvatar,
|
||||||
|
);
|
||||||
sewaList.refresh();
|
sewaList.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle sewa completion
|
// Handle sewa completion
|
||||||
void completeSewa(String id) {
|
void completeSewa(String id) async {
|
||||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
final sewa = sewaList[index];
|
||||||
sewa['status'] = 'Selesai';
|
sewaList[index] = SewaModel(
|
||||||
sewaList[index] = sewa;
|
id: sewa.id,
|
||||||
|
userId: sewa.userId,
|
||||||
|
status: 'Selesai',
|
||||||
|
waktuMulai: sewa.waktuMulai,
|
||||||
|
waktuSelesai: sewa.waktuSelesai,
|
||||||
|
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||||
|
tipePesanan: sewa.tipePesanan,
|
||||||
|
kuantitas: sewa.kuantitas,
|
||||||
|
asetId: sewa.asetId,
|
||||||
|
asetNama: sewa.asetNama,
|
||||||
|
asetFoto: sewa.asetFoto,
|
||||||
|
paketId: sewa.paketId,
|
||||||
|
paketNama: sewa.paketNama,
|
||||||
|
paketFoto: sewa.paketFoto,
|
||||||
|
totalTagihan: sewa.totalTagihan,
|
||||||
|
wargaNama: sewa.wargaNama,
|
||||||
|
wargaNoHp: sewa.wargaNoHp,
|
||||||
|
wargaAvatar: sewa.wargaAvatar,
|
||||||
|
);
|
||||||
sewaList.refresh();
|
sewaList.refresh();
|
||||||
|
// Update status in database
|
||||||
|
final asetProvider = Get.find<AsetProvider>();
|
||||||
|
await asetProvider.updateSewaAsetStatus(
|
||||||
|
sewaAsetId: id,
|
||||||
|
status: 'SELESAI',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark rental as returned
|
// Mark rental as returned
|
||||||
void markAsReturned(String id) {
|
Future<void> markAsReturned(String id) async {
|
||||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
final index = sewaList.indexWhere((sewa) => sewa.id == id);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
final sewa = sewaList[index];
|
||||||
sewa['status'] = 'Dikembalikan';
|
sewaList[index] = SewaModel(
|
||||||
sewaList[index] = sewa;
|
id: sewa.id,
|
||||||
|
userId: sewa.userId,
|
||||||
|
status: 'Dikembalikan',
|
||||||
|
waktuMulai: sewa.waktuMulai,
|
||||||
|
waktuSelesai: sewa.waktuSelesai,
|
||||||
|
tanggalPemesanan: sewa.tanggalPemesanan,
|
||||||
|
tipePesanan: sewa.tipePesanan,
|
||||||
|
kuantitas: sewa.kuantitas,
|
||||||
|
asetId: sewa.asetId,
|
||||||
|
asetNama: sewa.asetNama,
|
||||||
|
asetFoto: sewa.asetFoto,
|
||||||
|
paketId: sewa.paketId,
|
||||||
|
paketNama: sewa.paketNama,
|
||||||
|
paketFoto: sewa.paketFoto,
|
||||||
|
totalTagihan: sewa.totalTagihan,
|
||||||
|
wargaNama: sewa.wargaNama,
|
||||||
|
wargaNoHp: sewa.wargaNoHp,
|
||||||
|
wargaAvatar: sewa.wargaAvatar,
|
||||||
|
);
|
||||||
sewaList.refresh();
|
sewaList.refresh();
|
||||||
|
// Update status in database
|
||||||
|
final asetProvider = Get.find<AsetProvider>();
|
||||||
|
final result = await asetProvider.updateSewaAsetStatus(
|
||||||
|
sewaAsetId: id,
|
||||||
|
status: 'DIKEMBALIKAN',
|
||||||
|
);
|
||||||
|
if (!result) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Gagal',
|
||||||
|
'Gagal mengubah status sewa di database',
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ambil detail item paket (nama aset & kuantitas)
|
||||||
|
Future<List<Map<String, dynamic>>> getPaketItems(String paketId) async {
|
||||||
|
final asetProvider = Get.find<AsetProvider>();
|
||||||
|
debugPrint('[DEBUG] getPaketItems called with paketId: $paketId');
|
||||||
|
try {
|
||||||
|
final items = await asetProvider.getPaketItems(paketId);
|
||||||
|
debugPrint('[DEBUG] getPaketItems result for paketId $paketId:');
|
||||||
|
for (var item in items) {
|
||||||
|
debugPrint(' - item: ${item.toString()}');
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
} catch (e, stack) {
|
||||||
|
debugPrint('[ERROR] getPaketItems failed for paketId $paketId: $e');
|
||||||
|
debugPrint('[ERROR] Stacktrace: $stack');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RxBool getIsFullPayment(String sewaId) {
|
||||||
|
if (!isFullPaymentMap.containsKey(sewaId)) {
|
||||||
|
isFullPaymentMap[sewaId] = false.obs;
|
||||||
|
}
|
||||||
|
return isFullPaymentMap[sewaId]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextEditingController getNominalController(String sewaId) {
|
||||||
|
if (!nominalControllerMap.containsKey(sewaId)) {
|
||||||
|
final controller = TextEditingController(text: '0');
|
||||||
|
nominalControllerMap[sewaId] = controller;
|
||||||
|
}
|
||||||
|
return nominalControllerMap[sewaId]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFullPayment(String sewaId, bool value, num totalTagihan) {
|
||||||
|
getIsFullPayment(sewaId).value = value;
|
||||||
|
if (value) {
|
||||||
|
getNominalController(sewaId).text = totalTagihan.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RxString getPaymentMethod(String sewaId) {
|
||||||
|
if (!paymentMethodMap.containsKey(sewaId)) {
|
||||||
|
paymentMethodMap[sewaId] = 'Tunai'.obs;
|
||||||
|
}
|
||||||
|
return paymentMethodMap[sewaId]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPaymentMethod(String sewaId, String method) {
|
||||||
|
getPaymentMethod(sewaId).value = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getTagihanSewaIdBySewaAsetId(String sewaAsetId) async {
|
||||||
|
final asetProvider = Get.find<AsetProvider>();
|
||||||
|
final tagihan = await asetProvider.getTagihanSewa(sewaAsetId);
|
||||||
|
if (tagihan != null && tagihan['id'] != null) {
|
||||||
|
return tagihan['id'] as String;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> confirmPembayaranTagihan({
|
||||||
|
required String sewaAsetId,
|
||||||
|
required int nominal,
|
||||||
|
required String metodePembayaran,
|
||||||
|
}) async {
|
||||||
|
final tagihanSewaId = await getTagihanSewaIdBySewaAsetId(sewaAsetId);
|
||||||
|
if (tagihanSewaId == null) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Gagal',
|
||||||
|
'Tagihan sewa tidak ditemukan',
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final asetProvider = Get.find<AsetProvider>();
|
||||||
|
// Cek status sewa_aset saat ini
|
||||||
|
final sewaAsetData = await asetProvider.getSewaAsetWithAsetData(sewaAsetId);
|
||||||
|
if (sewaAsetData != null &&
|
||||||
|
(sewaAsetData['status']?.toString()?.toUpperCase() ==
|
||||||
|
'PERIKSA PEMBAYARAN')) {
|
||||||
|
// Ubah status menjadi MENUNGGU PEMBAYARAN
|
||||||
|
await asetProvider.updateSewaAsetStatus(
|
||||||
|
sewaAsetId: sewaAsetId,
|
||||||
|
status: 'MENUNGGU PEMBAYARAN',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final result = await asetProvider.processPembayaranTagihan(
|
||||||
|
tagihanSewaId: tagihanSewaId,
|
||||||
|
nominal: nominal,
|
||||||
|
metodePembayaran: metodePembayaran,
|
||||||
|
);
|
||||||
|
if (result) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Sukses',
|
||||||
|
'Pembayaran berhasil diproses',
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Get.snackbar(
|
||||||
|
'Gagal',
|
||||||
|
'Pembayaran gagal diproses',
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,187 @@
|
|||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:bumrent_app/app/data/models/aset_model.dart';
|
||||||
|
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||||
|
|
||||||
class PetugasTambahAsetController extends GetxController {
|
class PetugasTambahAsetController extends GetxController {
|
||||||
|
// Flag to check if in edit mode
|
||||||
|
final isEditing = false.obs;
|
||||||
|
String? assetId; // To store the ID of the asset being edited
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onInit() async {
|
||||||
|
super.onInit();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle edit mode and load data if needed
|
||||||
|
final args = Get.arguments;
|
||||||
|
debugPrint('[DEBUG] PetugasTambahAsetController initialized with args: $args');
|
||||||
|
|
||||||
|
if (args != null && args is Map<String, dynamic>) {
|
||||||
|
isEditing.value = args['isEditing'] ?? false;
|
||||||
|
debugPrint('[DEBUG] isEditing set to: ${isEditing.value}');
|
||||||
|
|
||||||
|
if (isEditing.value) {
|
||||||
|
// Get asset ID from arguments
|
||||||
|
final assetId = args['assetId']?.toString() ?? '';
|
||||||
|
debugPrint('[DEBUG] Edit mode: Loading asset with ID: $assetId');
|
||||||
|
|
||||||
|
if (assetId.isNotEmpty) {
|
||||||
|
// Store the asset ID and load asset data
|
||||||
|
this.assetId = assetId;
|
||||||
|
debugPrint('[DEBUG] Asset ID set to: $assetId');
|
||||||
|
|
||||||
|
// Load asset data and await completion
|
||||||
|
await _loadAssetData(assetId);
|
||||||
|
} else {
|
||||||
|
debugPrint('[ERROR] Edit mode but no assetId provided in arguments');
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'ID Aset tidak ditemukan',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
// Optionally navigate back if in edit mode without an ID
|
||||||
|
Future.delayed(Duration.zero, () => Get.back());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Set default values for new asset
|
||||||
|
debugPrint('[DEBUG] Add new asset mode');
|
||||||
|
quantityController.text = '1';
|
||||||
|
unitOfMeasureController.text = 'Unit';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default values for new asset when no arguments are passed
|
||||||
|
debugPrint('[DEBUG] No arguments passed, defaulting to add new asset mode');
|
||||||
|
quantityController.text = '1';
|
||||||
|
unitOfMeasureController.text = 'Unit';
|
||||||
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
debugPrint('[ERROR] Error in onInit: $e');
|
||||||
|
debugPrint('Stack trace: $stackTrace');
|
||||||
|
// Ensure loading is set to false even if there's an error
|
||||||
|
isLoading.value = false;
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Terjadi kesalahan saat memuat data',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen to field changes for validation
|
||||||
|
nameController.addListener(validateForm);
|
||||||
|
descriptionController.addListener(validateForm);
|
||||||
|
quantityController.addListener(validateForm);
|
||||||
|
pricePerHourController.addListener(validateForm);
|
||||||
|
pricePerDayController.addListener(validateForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
||||||
|
final isLoading = false.obs;
|
||||||
|
|
||||||
|
Future<void> _loadAssetData(String assetId) async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
debugPrint('[DEBUG] Fetching asset data for ID: $assetId');
|
||||||
|
|
||||||
|
// Fetch asset data from Supabase
|
||||||
|
final aset = await _asetProvider.getAsetById(assetId);
|
||||||
|
|
||||||
|
if (aset == null) {
|
||||||
|
throw Exception('Aset tidak ditemukan');
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('[DEBUG] Successfully fetched asset data: ${aset.toJson()}');
|
||||||
|
|
||||||
|
// Populate form fields with the fetched data
|
||||||
|
nameController.text = aset.nama ?? '';
|
||||||
|
descriptionController.text = aset.deskripsi ?? '';
|
||||||
|
quantityController.text = (aset.kuantitas ?? 1).toString();
|
||||||
|
|
||||||
|
// Ensure the status matches one of the available options exactly
|
||||||
|
final status = aset.status?.toLowerCase() ?? 'tersedia';
|
||||||
|
if (status == 'tersedia') {
|
||||||
|
selectedStatus.value = 'Tersedia';
|
||||||
|
} else if (status == 'pemeliharaan') {
|
||||||
|
selectedStatus.value = 'Pemeliharaan';
|
||||||
|
} else {
|
||||||
|
// Default to 'Tersedia' if status is not recognized
|
||||||
|
selectedStatus.value = 'Tersedia';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle time options and pricing
|
||||||
|
if (aset.satuanWaktuSewa != null && aset.satuanWaktuSewa!.isNotEmpty) {
|
||||||
|
// Reset time options
|
||||||
|
timeOptions.forEach((key, value) => value.value = false);
|
||||||
|
|
||||||
|
// Process each satuan waktu sewa
|
||||||
|
for (var sws in aset.satuanWaktuSewa) {
|
||||||
|
final satuan = sws['nama_satuan_waktu']?.toString().toLowerCase() ?? '';
|
||||||
|
final harga = sws['harga'] as int? ?? 0;
|
||||||
|
final maksimalWaktu = sws['maksimal_waktu'] as int? ?? 24;
|
||||||
|
|
||||||
|
if (satuan.contains('jam')) {
|
||||||
|
timeOptions['Per Jam']?.value = true;
|
||||||
|
pricePerHourController.text = harga.toString();
|
||||||
|
maxHourController.text = maksimalWaktu.toString();
|
||||||
|
} else if (satuan.contains('hari')) {
|
||||||
|
timeOptions['Per Hari']?.value = true;
|
||||||
|
pricePerDayController.text = harga.toString();
|
||||||
|
maxDayController.text = maksimalWaktu.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing images
|
||||||
|
selectedImages.clear();
|
||||||
|
networkImageUrls.clear();
|
||||||
|
|
||||||
|
// Get all image URLs from the model
|
||||||
|
final allImageUrls = aset.imageUrls.toList();
|
||||||
|
|
||||||
|
// If no imageUrls but has imageUrl, use that as fallback (backward compatibility)
|
||||||
|
if (allImageUrls.isEmpty && aset.imageUrl != null && aset.imageUrl!.isNotEmpty) {
|
||||||
|
allImageUrls.add(aset.imageUrl!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all images to the lists
|
||||||
|
for (final imageUrl in allImageUrls) {
|
||||||
|
if (imageUrl != null && imageUrl.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
// For network images, we'll store the URL in networkImageUrls
|
||||||
|
// and create a dummy XFile with the URL as path for backward compatibility
|
||||||
|
final dummyFile = XFile(imageUrl);
|
||||||
|
selectedImages.add(dummyFile);
|
||||||
|
networkImageUrls.add(imageUrl);
|
||||||
|
debugPrint('Added network image: $imageUrl');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error adding network image: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('Total ${networkImageUrls.length} images loaded for asset $assetId');
|
||||||
|
debugPrint('[DEBUG] Successfully loaded asset data for ID: $assetId');
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
debugPrint('[ERROR] Failed to load asset data: $e');
|
||||||
|
debugPrint('Stack trace: $stackTrace');
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Gagal memuat data aset: ${e.toString()}',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Optionally navigate back if there's an error
|
||||||
|
Future.delayed(const Duration(seconds: 2), () => Get.back());
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Form controllers
|
// Form controllers
|
||||||
final nameController = TextEditingController();
|
final nameController = TextEditingController();
|
||||||
final descriptionController = TextEditingController();
|
final descriptionController = TextEditingController();
|
||||||
@ -23,27 +203,17 @@ class PetugasTambahAsetController extends GetxController {
|
|||||||
final categoryOptions = ['Sewa', 'Langganan'];
|
final categoryOptions = ['Sewa', 'Langganan'];
|
||||||
final statusOptions = ['Tersedia', 'Pemeliharaan'];
|
final statusOptions = ['Tersedia', 'Pemeliharaan'];
|
||||||
|
|
||||||
// Images
|
// List to store selected images
|
||||||
final selectedImages = <String>[].obs;
|
final RxList<XFile> selectedImages = <XFile>[].obs;
|
||||||
|
// List to store network image URLs
|
||||||
|
final RxList<String> networkImageUrls = <String>[].obs;
|
||||||
|
final _picker = ImagePicker();
|
||||||
|
|
||||||
// Form validation
|
// Form validation
|
||||||
final isFormValid = false.obs;
|
final isFormValid = false.obs;
|
||||||
final isSubmitting = false.obs;
|
final isSubmitting = false.obs;
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
// Set default values
|
|
||||||
quantityController.text = '1';
|
|
||||||
unitOfMeasureController.text = 'Unit';
|
|
||||||
|
|
||||||
// Listen to field changes for validation
|
|
||||||
nameController.addListener(validateForm);
|
|
||||||
descriptionController.addListener(validateForm);
|
|
||||||
quantityController.addListener(validateForm);
|
|
||||||
pricePerHourController.addListener(validateForm);
|
|
||||||
pricePerDayController.addListener(validateForm);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
@ -89,17 +259,140 @@ class PetugasTambahAsetController extends GetxController {
|
|||||||
validateForm();
|
validateForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add image to the list (in a real app, this would handle file upload)
|
|
||||||
void addImage(String imagePath) {
|
// Create a new asset in Supabase
|
||||||
selectedImages.add(imagePath);
|
Future<String?> _createAsset(
|
||||||
validateForm();
|
Map<String, dynamic> assetData,
|
||||||
|
List<Map<String, dynamic>> satuanWaktuSewa,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
// Create the asset in the 'aset' table
|
||||||
|
final response = await _asetProvider.createAset(assetData);
|
||||||
|
|
||||||
|
if (response == null || response['id'] == null) {
|
||||||
|
debugPrint('❌ Failed to create asset: No response or ID from server');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove image from the list
|
final String assetId = response['id'].toString();
|
||||||
|
debugPrint('✅ Asset created with ID: $assetId');
|
||||||
|
|
||||||
|
// Add satuan waktu sewa
|
||||||
|
for (var sws in satuanWaktuSewa) {
|
||||||
|
final success = await _asetProvider.addSatuanWaktuSewa(
|
||||||
|
asetId: assetId,
|
||||||
|
satuanWaktu: sws['satuan_waktu'],
|
||||||
|
harga: sws['harga'],
|
||||||
|
maksimalWaktu: sws['maksimal_waktu'],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
debugPrint('❌ Failed to add satuan waktu sewa: $sws');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return assetId;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
debugPrint('❌ Error creating asset: $e');
|
||||||
|
debugPrint('Stack trace: $stackTrace');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update an existing asset in Supabase
|
||||||
|
Future<bool> _updateAsset(
|
||||||
|
String assetId,
|
||||||
|
Map<String, dynamic> assetData,
|
||||||
|
List<Map<String, dynamic>> satuanWaktuSewa,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
debugPrint('\n🔄 Starting update for asset ID: $assetId');
|
||||||
|
|
||||||
|
// 1. Extract and remove foto_aset from assetData as it's not in the aset table
|
||||||
|
final fotoAsetUrl = assetData['foto_aset'];
|
||||||
|
assetData.remove('foto_aset');
|
||||||
|
debugPrint('📝 Asset data prepared for update (without foto_aset)');
|
||||||
|
|
||||||
|
// 2. Update the main asset data (without foto_aset)
|
||||||
|
debugPrint('🔄 Updating main asset data...');
|
||||||
|
final success = await _asetProvider.updateAset(assetId, assetData);
|
||||||
|
if (!success) {
|
||||||
|
debugPrint('❌ Failed to update asset with ID: $assetId');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
debugPrint('✅ Successfully updated main asset data');
|
||||||
|
|
||||||
|
// 3. Update satuan waktu sewa
|
||||||
|
debugPrint('\n🔄 Updating rental time units...');
|
||||||
|
// First, delete existing satuan waktu sewa
|
||||||
|
await _asetProvider.deleteSatuanWaktuSewaByAsetId(assetId);
|
||||||
|
|
||||||
|
// Then add the new ones
|
||||||
|
for (var sws in satuanWaktuSewa) {
|
||||||
|
debugPrint(' - Adding: ${sws['satuan_waktu']} (${sws['harga']} IDR)');
|
||||||
|
await _asetProvider.addSatuanWaktuSewa(
|
||||||
|
asetId: assetId,
|
||||||
|
satuanWaktu: sws['satuan_waktu'],
|
||||||
|
harga: sws['harga'] as int,
|
||||||
|
maksimalWaktu: sws['maksimal_waktu'] as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
debugPrint('✅ Successfully updated rental time units');
|
||||||
|
|
||||||
|
// 4. Update photos in the foto_aset table if any exist
|
||||||
|
if (selectedImages.isNotEmpty || networkImageUrls.isNotEmpty) {
|
||||||
|
// Combine network URLs and local file paths
|
||||||
|
final List<String> allImageUrls = [
|
||||||
|
...networkImageUrls,
|
||||||
|
...selectedImages.map((file) => file.path),
|
||||||
|
];
|
||||||
|
|
||||||
|
debugPrint('\n🖼️ Processing photos for asset $assetId');
|
||||||
|
debugPrint(' - Network URLs: ${networkImageUrls.length}');
|
||||||
|
debugPrint(' - Local files: ${selectedImages.length}');
|
||||||
|
debugPrint(' - Total unique photos: ${allImageUrls.toSet().length} (before deduplication)');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use updateFotoAset which handles both uploading new photos and updating the database
|
||||||
|
final photoSuccess = await _asetProvider.updateFotoAset(
|
||||||
|
asetId: assetId,
|
||||||
|
fotoUrls: allImageUrls,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!photoSuccess) {
|
||||||
|
debugPrint('⚠️ Some photos might not have been updated for asset $assetId');
|
||||||
|
// We don't fail the whole update if photo update fails
|
||||||
|
// as the main asset data has been saved successfully
|
||||||
|
} else {
|
||||||
|
debugPrint('✅ Successfully updated photos for asset $assetId');
|
||||||
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
debugPrint('❌ Error updating photos: $e');
|
||||||
|
debugPrint('Stack trace: $stackTrace');
|
||||||
|
// Continue with the update even if photo update fails
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint('ℹ️ No photos to update');
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('\n✅ Asset update completed successfully for ID: $assetId');
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
debugPrint('❌ Error updating asset: $e');
|
||||||
|
debugPrint('Stack trace: $stackTrace');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove an image from the selected images list
|
||||||
void removeImage(int index) {
|
void removeImage(int index) {
|
||||||
if (index >= 0 && index < selectedImages.length) {
|
if (index >= 0 && index < selectedImages.length) {
|
||||||
|
// Remove from both lists if they have an entry at this index
|
||||||
|
if (index < networkImageUrls.length) {
|
||||||
|
networkImageUrls.removeAt(index);
|
||||||
|
}
|
||||||
selectedImages.removeAt(index);
|
selectedImages.removeAt(index);
|
||||||
validateForm();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,62 +426,130 @@ class PetugasTambahAsetController extends GetxController {
|
|||||||
basicValid && perHourValid && perDayValid && anyTimeOptionSelected;
|
basicValid && perHourValid && perDayValid && anyTimeOptionSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit form and save asset
|
// Submit form and save or update asset
|
||||||
Future<void> saveAsset() async {
|
Future<void> saveAsset() async {
|
||||||
if (!isFormValid.value) return;
|
if (!isFormValid.value) return;
|
||||||
|
|
||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// In a real app, this would make an API call to save the asset
|
// Prepare the basic asset data
|
||||||
await Future.delayed(const Duration(seconds: 1)); // Mock API call
|
final Map<String, dynamic> assetData = {
|
||||||
|
|
||||||
// Prepare asset data
|
|
||||||
final assetData = {
|
|
||||||
'nama': nameController.text,
|
'nama': nameController.text,
|
||||||
'deskripsi': descriptionController.text,
|
'deskripsi': descriptionController.text,
|
||||||
'kategori': selectedCategory.value,
|
'kategori': 'sewa', // Default to 'sewa' category
|
||||||
'status': selectedStatus.value,
|
'status': selectedStatus.value,
|
||||||
'kuantitas': int.parse(quantityController.text),
|
'kuantitas': int.parse(quantityController.text),
|
||||||
'satuan_ukur': unitOfMeasureController.text,
|
'satuan_ukur': 'unit', // Default unit of measure
|
||||||
'opsi_waktu_sewa':
|
|
||||||
timeOptions.entries
|
|
||||||
.where((entry) => entry.value.value)
|
|
||||||
.map((entry) => entry.key)
|
|
||||||
.toList(),
|
|
||||||
'harga_per_jam':
|
|
||||||
timeOptions['Per Jam']!.value
|
|
||||||
? int.parse(pricePerHourController.text)
|
|
||||||
: null,
|
|
||||||
'max_jam':
|
|
||||||
timeOptions['Per Jam']!.value && maxHourController.text.isNotEmpty
|
|
||||||
? int.parse(maxHourController.text)
|
|
||||||
: null,
|
|
||||||
'harga_per_hari':
|
|
||||||
timeOptions['Per Hari']!.value
|
|
||||||
? int.parse(pricePerDayController.text)
|
|
||||||
: null,
|
|
||||||
'max_hari':
|
|
||||||
timeOptions['Per Hari']!.value && maxDayController.text.isNotEmpty
|
|
||||||
? int.parse(maxDayController.text)
|
|
||||||
: null,
|
|
||||||
'gambar': selectedImages,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Log the data (in a real app, this would be sent to an API)
|
// Handle time options and pricing
|
||||||
print('Asset data: $assetData');
|
final List<Map<String, dynamic>> satuanWaktuSewa = [];
|
||||||
|
|
||||||
// Return to the asset list page
|
if (timeOptions['Per Jam']?.value == true) {
|
||||||
Get.back();
|
final hargaPerJam = int.tryParse(pricePerHourController.text) ?? 0;
|
||||||
|
final maxJam = int.tryParse(maxHourController.text) ?? 24;
|
||||||
|
|
||||||
|
if (hargaPerJam <= 0) {
|
||||||
|
throw Exception('Harga per jam harus lebih dari 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
satuanWaktuSewa.add({
|
||||||
|
'satuan_waktu': 'jam',
|
||||||
|
'harga': hargaPerJam,
|
||||||
|
'maksimal_waktu': maxJam,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeOptions['Per Hari']?.value == true) {
|
||||||
|
final hargaPerHari = int.tryParse(pricePerDayController.text) ?? 0;
|
||||||
|
final maxHari = int.tryParse(maxDayController.text) ?? 30;
|
||||||
|
|
||||||
|
if (hargaPerHari <= 0) {
|
||||||
|
throw Exception('Harga per hari harus lebih dari 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
satuanWaktuSewa.add({
|
||||||
|
'satuan_waktu': 'hari',
|
||||||
|
'harga': hargaPerHari,
|
||||||
|
'maksimal_waktu': maxHari,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that at least one time option is selected
|
||||||
|
if (satuanWaktuSewa.isEmpty) {
|
||||||
|
throw Exception('Pilih setidaknya satu opsi waktu sewa (jam/hari)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle image uploads
|
||||||
|
List<String> imageUrls = [];
|
||||||
|
|
||||||
|
if (networkImageUrls.isNotEmpty) {
|
||||||
|
// Use existing network URLs
|
||||||
|
imageUrls = List.from(networkImageUrls);
|
||||||
|
} else if (selectedImages.isNotEmpty) {
|
||||||
|
// For local files, we'll upload them to Supabase Storage
|
||||||
|
// Store the file paths for now, they'll be uploaded in the provider
|
||||||
|
imageUrls = selectedImages.map((file) => file.path).toList();
|
||||||
|
debugPrint('Found ${imageUrls.length} local images to upload');
|
||||||
|
} else if (!isEditing.value) {
|
||||||
|
// For new assets, require at least one image
|
||||||
|
throw Exception('Harap unggah setidaknya satu gambar');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure at least one image is provided for new assets
|
||||||
|
if (imageUrls.isEmpty && !isEditing.value) {
|
||||||
|
throw Exception('Harap unggah setidaknya satu gambar');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update the asset
|
||||||
|
bool success;
|
||||||
|
String? createdAssetId;
|
||||||
|
|
||||||
|
if (isEditing.value && (assetId?.isNotEmpty ?? false)) {
|
||||||
|
// Update existing asset
|
||||||
|
debugPrint('🔄 Updating asset with ID: $assetId');
|
||||||
|
success = await _updateAsset(assetId!, assetData, satuanWaktuSewa);
|
||||||
|
|
||||||
|
// Update all photos if we have any
|
||||||
|
if (success && imageUrls.isNotEmpty) {
|
||||||
|
await _asetProvider.updateFotoAset(
|
||||||
|
asetId: assetId!,
|
||||||
|
fotoUrls: imageUrls,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new asset
|
||||||
|
debugPrint('🔄 Creating new asset');
|
||||||
|
createdAssetId = await _createAsset(assetData, satuanWaktuSewa);
|
||||||
|
success = createdAssetId != null;
|
||||||
|
|
||||||
|
// Add all photos for new asset
|
||||||
|
if (success && createdAssetId != null && imageUrls.isNotEmpty) {
|
||||||
|
await _asetProvider.updateFotoAset(
|
||||||
|
asetId: createdAssetId,
|
||||||
|
fotoUrls: imageUrls,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
// Show success message
|
// Show success message
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Berhasil',
|
'Sukses',
|
||||||
'Aset berhasil ditambahkan',
|
isEditing.value ? 'Aset berhasil diperbarui' : 'Aset berhasil ditambahkan',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
colorText: Colors.white,
|
colorText: Colors.white,
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
duration: const Duration(seconds: 3),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Navigate back with success after a short delay
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
Get.back(result: true);
|
||||||
|
} else {
|
||||||
|
throw Exception('Gagal menyimpan aset');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Show error message
|
// Show error message
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
@ -203,8 +564,68 @@ class PetugasTambahAsetController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Example method to upload images (to be implemented with your backend)
|
||||||
|
// Future<List<String>> _uploadImages(List<XFile> images) async {
|
||||||
|
// List<String> urls = [];
|
||||||
|
// for (var image in images) {
|
||||||
|
// // Upload image to your server and get the URL
|
||||||
|
// // final url = await yourApiService.uploadImage(File(image.path));
|
||||||
|
// // urls.add(url);
|
||||||
|
// urls.add('https://example.com/path/to/uploaded/image.jpg'); // Mock URL
|
||||||
|
// }
|
||||||
|
// return urls;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Pick image from camera
|
||||||
|
Future<void> pickImageFromCamera() async {
|
||||||
|
try {
|
||||||
|
final XFile? image = await _picker.pickImage(
|
||||||
|
source: ImageSource.camera,
|
||||||
|
imageQuality: 80,
|
||||||
|
maxWidth: 1024,
|
||||||
|
maxHeight: 1024,
|
||||||
|
);
|
||||||
|
if (image != null) {
|
||||||
|
selectedImages.add(image);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Gagal mengambil gambar dari kamera: $e',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick image from gallery
|
||||||
|
Future<void> pickImageFromGallery() async {
|
||||||
|
try {
|
||||||
|
final List<XFile>? images = await _picker.pickMultiImage(
|
||||||
|
imageQuality: 80,
|
||||||
|
maxWidth: 1024,
|
||||||
|
maxHeight: 1024,
|
||||||
|
);
|
||||||
|
if (images != null && images.isNotEmpty) {
|
||||||
|
selectedImages.addAll(images);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Gagal memilih gambar dari galeri: $e',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For demonstration purposes: add sample image
|
// For demonstration purposes: add sample image
|
||||||
void addSampleImage() {
|
void addSampleImage() {
|
||||||
addImage('assets/images/sample_asset_${selectedImages.length + 1}.jpg');
|
// In a real app, this would open the image picker
|
||||||
|
selectedImages.add(XFile('assets/images/sample_asset_${selectedImages.length + 1}.jpg'));
|
||||||
|
validateForm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:bumrent_app/app/data/models/paket_model.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class PetugasTambahPaketController extends GetxController {
|
class PetugasTambahPaketController extends GetxController {
|
||||||
// Form controllers
|
// Form controllers
|
||||||
@ -10,14 +16,14 @@ class PetugasTambahPaketController extends GetxController {
|
|||||||
|
|
||||||
// Dropdown and toggle values
|
// Dropdown and toggle values
|
||||||
final selectedCategory = 'Bulanan'.obs;
|
final selectedCategory = 'Bulanan'.obs;
|
||||||
final selectedStatus = 'Aktif'.obs;
|
final selectedStatus = 'Tersedia'.obs;
|
||||||
|
|
||||||
// Category options
|
// Category options
|
||||||
final categoryOptions = ['Bulanan', 'Tahunan', 'Premium', 'Bisnis'];
|
final categoryOptions = ['Bulanan', 'Tahunan', 'Premium', 'Bisnis'];
|
||||||
final statusOptions = ['Aktif', 'Nonaktif'];
|
final statusOptions = ['Tersedia', 'Pemeliharaan'];
|
||||||
|
|
||||||
// Images
|
// Images
|
||||||
final selectedImages = <String>[].obs;
|
final selectedImages = <dynamic>[].obs;
|
||||||
|
|
||||||
// For package name and description
|
// For package name and description
|
||||||
final packageNameController = TextEditingController();
|
final packageNameController = TextEditingController();
|
||||||
@ -31,21 +37,85 @@ class PetugasTambahPaketController extends GetxController {
|
|||||||
// For asset selection
|
// For asset selection
|
||||||
final RxList<Map<String, dynamic>> availableAssets =
|
final RxList<Map<String, dynamic>> availableAssets =
|
||||||
<Map<String, dynamic>>[].obs;
|
<Map<String, dynamic>>[].obs;
|
||||||
final Rx<int?> selectedAsset = Rx<int?>(null);
|
final Rx<String?> selectedAsset = Rx<String?>(null);
|
||||||
final RxBool isLoadingAssets = false.obs;
|
final RxBool isLoadingAssets = false.obs;
|
||||||
|
|
||||||
// Form validation
|
// Form validation
|
||||||
final isFormValid = false.obs;
|
final isFormValid = false.obs;
|
||||||
final isSubmitting = false.obs;
|
final isSubmitting = false.obs;
|
||||||
|
|
||||||
|
// New RxBool for editing
|
||||||
|
final isEditing = false.obs;
|
||||||
|
|
||||||
|
final timeOptions = {'Per Jam': true.obs, 'Per Hari': false.obs};
|
||||||
|
final pricePerHourController = TextEditingController();
|
||||||
|
final maxHourController = TextEditingController();
|
||||||
|
final pricePerDayController = TextEditingController();
|
||||||
|
final maxDayController = TextEditingController();
|
||||||
|
|
||||||
|
final _picker = ImagePicker();
|
||||||
|
|
||||||
|
final isFormChanged = false.obs;
|
||||||
|
Map<String, dynamic> initialFormData = {};
|
||||||
|
|
||||||
|
final AsetProvider _asetProvider = Get.put(AsetProvider());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
|
||||||
|
// Ambil flag isEditing dari arguments
|
||||||
|
isEditing.value =
|
||||||
|
Get.arguments != null && Get.arguments['isEditing'] == true;
|
||||||
|
|
||||||
|
if (isEditing.value) {
|
||||||
|
final paketArg = Get.arguments['paket'];
|
||||||
|
String? paketId;
|
||||||
|
if (paketArg != null) {
|
||||||
|
if (paketArg is Map && paketArg['id'] != null) {
|
||||||
|
paketId = paketArg['id'].toString();
|
||||||
|
} else if (paketArg is PaketModel && paketArg.id != null) {
|
||||||
|
paketId = paketArg.id.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (paketId != null) {
|
||||||
|
fetchPaketDetail(paketId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Listen to field changes for validation
|
// Listen to field changes for validation
|
||||||
nameController.addListener(validateForm);
|
nameController.addListener(() {
|
||||||
descriptionController.addListener(validateForm);
|
validateForm();
|
||||||
priceController.addListener(validateForm);
|
checkFormChanged();
|
||||||
|
});
|
||||||
|
descriptionController.addListener(() {
|
||||||
|
validateForm();
|
||||||
|
checkFormChanged();
|
||||||
|
});
|
||||||
|
priceController.addListener(() {
|
||||||
|
validateForm();
|
||||||
|
checkFormChanged();
|
||||||
|
});
|
||||||
|
itemQuantityController.addListener(() {
|
||||||
|
validateForm();
|
||||||
|
checkFormChanged();
|
||||||
|
});
|
||||||
|
pricePerHourController.addListener(() {
|
||||||
|
validateForm();
|
||||||
|
checkFormChanged();
|
||||||
|
});
|
||||||
|
maxHourController.addListener(() {
|
||||||
|
validateForm();
|
||||||
|
checkFormChanged();
|
||||||
|
});
|
||||||
|
pricePerDayController.addListener(() {
|
||||||
|
validateForm();
|
||||||
|
checkFormChanged();
|
||||||
|
});
|
||||||
|
maxDayController.addListener(() {
|
||||||
|
validateForm();
|
||||||
|
checkFormChanged();
|
||||||
|
});
|
||||||
|
|
||||||
// Load available assets when the controller initializes
|
// Load available assets when the controller initializes
|
||||||
fetchAvailableAssets();
|
fetchAvailableAssets();
|
||||||
@ -61,6 +131,10 @@ class PetugasTambahPaketController extends GetxController {
|
|||||||
packageNameController.dispose();
|
packageNameController.dispose();
|
||||||
packageDescriptionController.dispose();
|
packageDescriptionController.dispose();
|
||||||
packagePriceController.dispose();
|
packagePriceController.dispose();
|
||||||
|
pricePerHourController.dispose();
|
||||||
|
maxHourController.dispose();
|
||||||
|
pricePerDayController.dispose();
|
||||||
|
maxDayController.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,18 +142,21 @@ class PetugasTambahPaketController extends GetxController {
|
|||||||
void setCategory(String category) {
|
void setCategory(String category) {
|
||||||
selectedCategory.value = category;
|
selectedCategory.value = category;
|
||||||
validateForm();
|
validateForm();
|
||||||
|
checkFormChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change selected status
|
// Change selected status
|
||||||
void setStatus(String status) {
|
void setStatus(String status) {
|
||||||
selectedStatus.value = status;
|
selectedStatus.value = status;
|
||||||
validateForm();
|
validateForm();
|
||||||
|
checkFormChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add image to the list (in a real app, this would handle file upload)
|
// Add image to the list (in a real app, this would handle file upload)
|
||||||
void addImage(String imagePath) {
|
void addImage(String imagePath) {
|
||||||
selectedImages.add(imagePath);
|
selectedImages.add(imagePath);
|
||||||
validateForm();
|
validateForm();
|
||||||
|
checkFormChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove image from the list
|
// Remove image from the list
|
||||||
@ -87,34 +164,43 @@ class PetugasTambahPaketController extends GetxController {
|
|||||||
if (index >= 0 && index < selectedImages.length) {
|
if (index >= 0 && index < selectedImages.length) {
|
||||||
selectedImages.removeAt(index);
|
selectedImages.removeAt(index);
|
||||||
validateForm();
|
validateForm();
|
||||||
|
checkFormChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch available assets from the API or local data
|
// Fetch available assets from Supabase and filter out already selected ones
|
||||||
void fetchAvailableAssets() {
|
Future<void> fetchAvailableAssets() async {
|
||||||
isLoadingAssets.value = true;
|
isLoadingAssets.value = true;
|
||||||
|
try {
|
||||||
// This is a mock implementation - replace with actual API call
|
final allAssets = await _asetProvider.getSewaAsets();
|
||||||
Future.delayed(const Duration(seconds: 1), () {
|
final selectedAsetIds =
|
||||||
availableAssets.value = [
|
packageItems.map((item) => item['asetId'].toString()).toSet();
|
||||||
{'id': 1, 'nama': 'Laptop Dell XPS', 'stok': 5},
|
// Only show assets not yet selected
|
||||||
{'id': 2, 'nama': 'Proyektor Epson', 'stok': 3},
|
availableAssets.value =
|
||||||
{'id': 3, 'nama': 'Meja Kantor', 'stok': 10},
|
allAssets
|
||||||
{'id': 4, 'nama': 'Kursi Ergonomis', 'stok': 15},
|
.where((aset) => !selectedAsetIds.contains(aset.id))
|
||||||
{'id': 5, 'nama': 'Printer HP LaserJet', 'stok': 2},
|
.map(
|
||||||
{'id': 6, 'nama': 'AC Panasonic 1PK', 'stok': 8},
|
(aset) => {
|
||||||
];
|
'id': aset.id,
|
||||||
|
'nama': aset.nama,
|
||||||
|
'stok': aset.kuantitas,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
availableAssets.value = [];
|
||||||
|
} finally {
|
||||||
isLoadingAssets.value = false;
|
isLoadingAssets.value = false;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the selected asset
|
// Set the selected asset
|
||||||
void setSelectedAsset(int? assetId) {
|
void setSelectedAsset(String? assetId) {
|
||||||
selectedAsset.value = assetId;
|
selectedAsset.value = assetId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get remaining stock for an asset (considering current selections)
|
// Get remaining stock for an asset (considering current selections)
|
||||||
int getRemainingStock(int assetId) {
|
int getRemainingStock(String assetId) {
|
||||||
// Find the asset in available assets
|
// Find the asset in available assets
|
||||||
final asset = availableAssets.firstWhere(
|
final asset = availableAssets.firstWhere(
|
||||||
(item) => item['id'] == assetId,
|
(item) => item['id'] == assetId,
|
||||||
@ -129,7 +215,7 @@ class PetugasTambahPaketController extends GetxController {
|
|||||||
// Calculate how many of this asset are already in the package
|
// Calculate how many of this asset are already in the package
|
||||||
int alreadySelected = 0;
|
int alreadySelected = 0;
|
||||||
for (var item in packageItems) {
|
for (var item in packageItems) {
|
||||||
if (item['asetId'] == assetId) {
|
if (item['asetId'].toString() == assetId) {
|
||||||
alreadySelected += item['jumlah'] as int;
|
alreadySelected += item['jumlah'] as int;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,6 +290,8 @@ class PetugasTambahPaketController extends GetxController {
|
|||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
colorText: Colors.white,
|
colorText: Colors.white,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
checkFormChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update an existing package item
|
// Update an existing package item
|
||||||
@ -301,11 +389,16 @@ class PetugasTambahPaketController extends GetxController {
|
|||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
colorText: Colors.white,
|
colorText: Colors.white,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
checkFormChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove an item from the package
|
// Remove an item from the package
|
||||||
void removeItem(int index) {
|
void removeItem(int index) {
|
||||||
|
if (index >= 0 && index < packageItems.length) {
|
||||||
packageItems.removeAt(index);
|
packageItems.removeAt(index);
|
||||||
|
checkFormChanged();
|
||||||
|
}
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Dihapus',
|
'Dihapus',
|
||||||
'Item berhasil dihapus dari paket',
|
'Item berhasil dihapus dari paket',
|
||||||
@ -319,10 +412,7 @@ class PetugasTambahPaketController extends GetxController {
|
|||||||
void validateForm() {
|
void validateForm() {
|
||||||
// Basic validation
|
// Basic validation
|
||||||
bool basicValid =
|
bool basicValid =
|
||||||
nameController.text.isNotEmpty &&
|
nameController.text.isNotEmpty && descriptionController.text.isNotEmpty;
|
||||||
descriptionController.text.isNotEmpty &&
|
|
||||||
priceController.text.isNotEmpty &&
|
|
||||||
int.tryParse(priceController.text) != null;
|
|
||||||
|
|
||||||
// Package should have at least one item
|
// Package should have at least one item
|
||||||
bool hasItems = packageItems.isNotEmpty;
|
bool hasItems = packageItems.isNotEmpty;
|
||||||
@ -337,27 +427,191 @@ class PetugasTambahPaketController extends GetxController {
|
|||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// In a real app, this would make an API call to save the package
|
final supabase = Supabase.instance.client;
|
||||||
await Future.delayed(const Duration(seconds: 1)); // Mock API call
|
if (isEditing.value) {
|
||||||
|
// --- UPDATE LOGIC ---
|
||||||
|
final paketArg = Get.arguments['paket'];
|
||||||
|
final String paketId =
|
||||||
|
paketArg is Map && paketArg['id'] != null
|
||||||
|
? paketArg['id'].toString()
|
||||||
|
: (paketArg is PaketModel && paketArg.id != null
|
||||||
|
? paketArg.id.toString()
|
||||||
|
: '');
|
||||||
|
if (paketId.isEmpty) throw Exception('ID paket tidak ditemukan');
|
||||||
|
|
||||||
// Prepare package data
|
// 1. Update data utama paket
|
||||||
final paketData = {
|
await supabase
|
||||||
|
.from('paket')
|
||||||
|
.update({
|
||||||
'nama': nameController.text,
|
'nama': nameController.text,
|
||||||
'deskripsi': descriptionController.text,
|
'deskripsi': descriptionController.text,
|
||||||
'kategori': selectedCategory.value,
|
'status': selectedStatus.value.toLowerCase(),
|
||||||
'status': selectedStatus.value == 'Aktif',
|
})
|
||||||
'harga': int.parse(priceController.text),
|
.eq('id', paketId);
|
||||||
'gambar': selectedImages,
|
|
||||||
'items': packageItems,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Log the data (in a real app, this would be sent to an API)
|
// 2. Update paket_item: hapus semua, insert ulang
|
||||||
print('Package data: $paketData');
|
await supabase.from('paket_item').delete().eq('paket_id', paketId);
|
||||||
|
for (var item in packageItems) {
|
||||||
|
await supabase.from('paket_item').insert({
|
||||||
|
'paket_id': paketId,
|
||||||
|
'aset_id': item['asetId'],
|
||||||
|
'kuantitas': item['jumlah'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Return to the package list page
|
// 3. Update satuan_waktu_sewa: hapus semua, insert ulang
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.back();
|
||||||
|
|
||||||
// Show success message
|
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Berhasil',
|
'Berhasil',
|
||||||
'Paket berhasil ditambahkan',
|
'Paket berhasil ditambahkan',
|
||||||
@ -365,11 +619,12 @@ class PetugasTambahPaketController extends GetxController {
|
|||||||
colorText: Colors.white,
|
colorText: Colors.white,
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Show error message
|
// Show error message
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Gagal',
|
'Gagal',
|
||||||
'Terjadi kesalahan: ${e.toString()}',
|
'Terjadi kesalahan: \\${e.toString()}',
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
colorText: Colors.white,
|
colorText: Colors.white,
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
@ -390,4 +645,215 @@ class PetugasTambahPaketController extends GetxController {
|
|||||||
selectedImages.add('https://example.com/sample_image.jpg');
|
selectedImages.add('https://example.com/sample_image.jpg');
|
||||||
validateForm();
|
validateForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void toggleTimeOption(String option) {
|
||||||
|
timeOptions[option]?.value = !(timeOptions[option]?.value ?? false);
|
||||||
|
// Ensure at least one option is selected
|
||||||
|
bool anySelected = false;
|
||||||
|
timeOptions.forEach((key, value) {
|
||||||
|
if (value.value) anySelected = true;
|
||||||
|
});
|
||||||
|
if (!anySelected) {
|
||||||
|
timeOptions[option]?.value = true;
|
||||||
|
}
|
||||||
|
validateForm();
|
||||||
|
checkFormChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchPaketDetail(String paketId) async {
|
||||||
|
try {
|
||||||
|
debugPrint('[DEBUG] Fetching paket detail for id: $paketId');
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
// 1) Ambil data paket utama
|
||||||
|
final paketData =
|
||||||
|
await supabase
|
||||||
|
.from('paket')
|
||||||
|
.select('id, nama, deskripsi, status')
|
||||||
|
.eq('id', paketId)
|
||||||
|
.single();
|
||||||
|
debugPrint('[DEBUG] Paket data: ' + paketData.toString());
|
||||||
|
|
||||||
|
// 2) Ambil paket_item
|
||||||
|
final paketItemData = await supabase
|
||||||
|
.from('paket_item')
|
||||||
|
.select('id, paket_id, aset_id, kuantitas')
|
||||||
|
.eq('paket_id', paketId);
|
||||||
|
debugPrint('[DEBUG] Paket item data: ' + paketItemData.toString());
|
||||||
|
|
||||||
|
// 3) Ambil satuan_waktu_sewa
|
||||||
|
final swsData = await supabase
|
||||||
|
.from('satuan_waktu_sewa')
|
||||||
|
.select('id, paket_id, satuan_waktu_id, harga, maksimal_waktu')
|
||||||
|
.eq('paket_id', paketId);
|
||||||
|
debugPrint('[DEBUG] Satuan waktu sewa data: ' + swsData.toString());
|
||||||
|
|
||||||
|
// 4) Ambil semua satuan_waktu_id dari swsData
|
||||||
|
final swIds = swsData.map((e) => e['satuan_waktu_id']).toSet().toList();
|
||||||
|
final swData =
|
||||||
|
swIds.isNotEmpty
|
||||||
|
? await supabase
|
||||||
|
.from('satuan_waktu')
|
||||||
|
.select('id, nama_satuan_waktu')
|
||||||
|
.inFilter('id', swIds)
|
||||||
|
: [];
|
||||||
|
debugPrint('[DEBUG] Satuan waktu data: ' + swData.toString());
|
||||||
|
final Map satuanWaktuMap = {
|
||||||
|
for (var sw in swData) sw['id']: sw['nama_satuan_waktu'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5) Ambil foto_aset
|
||||||
|
final fotoData = await supabase
|
||||||
|
.from('foto_aset')
|
||||||
|
.select('id_paket, foto_aset')
|
||||||
|
.eq('id_paket', paketId);
|
||||||
|
debugPrint('[DEBUG] Foto aset data: ' + fotoData.toString());
|
||||||
|
|
||||||
|
// 6) Kumpulkan semua aset_id dari paketItemData
|
||||||
|
final asetIds = paketItemData.map((e) => e['aset_id']).toSet().toList();
|
||||||
|
final asetData =
|
||||||
|
asetIds.isNotEmpty
|
||||||
|
? await supabase
|
||||||
|
.from('aset')
|
||||||
|
.select('id, nama, kuantitas')
|
||||||
|
.inFilter('id', asetIds)
|
||||||
|
: [];
|
||||||
|
debugPrint('[DEBUG] Aset data: ' + asetData.toString());
|
||||||
|
final Map asetMap = {for (var a in asetData) a['id']: a};
|
||||||
|
|
||||||
|
// Prefill field controller
|
||||||
|
nameController.text = paketData['nama']?.toString() ?? '';
|
||||||
|
descriptionController.text = paketData['deskripsi']?.toString() ?? '';
|
||||||
|
// Status mapping
|
||||||
|
final statusDb =
|
||||||
|
(paketData['status']?.toString().toLowerCase() ?? 'tersedia');
|
||||||
|
selectedStatus.value =
|
||||||
|
statusDb == 'pemeliharaan' ? 'Pemeliharaan' : 'Tersedia';
|
||||||
|
|
||||||
|
// Foto
|
||||||
|
selectedImages.clear();
|
||||||
|
if (fotoData.isNotEmpty) {
|
||||||
|
for (var foto in fotoData) {
|
||||||
|
final url = foto['foto_aset']?.toString();
|
||||||
|
if (url != null && url.isNotEmpty) {
|
||||||
|
selectedImages.add(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item paket
|
||||||
|
packageItems.clear();
|
||||||
|
for (var item in paketItemData) {
|
||||||
|
final aset = asetMap[item['aset_id']];
|
||||||
|
packageItems.add({
|
||||||
|
'asetId': item['aset_id'],
|
||||||
|
'nama': aset != null ? aset['nama'] : '',
|
||||||
|
'jumlah': item['kuantitas'],
|
||||||
|
'stok': aset != null ? aset['kuantitas'] : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opsi waktu & harga sewa
|
||||||
|
// Reset
|
||||||
|
timeOptions['Per Jam']?.value = false;
|
||||||
|
timeOptions['Per Hari']?.value = false;
|
||||||
|
pricePerHourController.clear();
|
||||||
|
maxHourController.clear();
|
||||||
|
pricePerDayController.clear();
|
||||||
|
maxDayController.clear();
|
||||||
|
for (var sws in swsData) {
|
||||||
|
final satuanNama =
|
||||||
|
satuanWaktuMap[sws['satuan_waktu_id']]?.toString().toLowerCase() ??
|
||||||
|
'';
|
||||||
|
if (satuanNama.contains('jam')) {
|
||||||
|
timeOptions['Per Jam']?.value = true;
|
||||||
|
pricePerHourController.text = (sws['harga'] ?? '').toString();
|
||||||
|
maxHourController.text = (sws['maksimal_waktu'] ?? '').toString();
|
||||||
|
} else if (satuanNama.contains('hari')) {
|
||||||
|
timeOptions['Per Hari']?.value = true;
|
||||||
|
pricePerDayController.text = (sws['harga'] ?? '').toString();
|
||||||
|
maxDayController.text = (sws['maksimal_waktu'] ?? '').toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan snapshot initialFormData setelah prefill
|
||||||
|
initialFormData = {
|
||||||
|
'nama': nameController.text,
|
||||||
|
'deskripsi': descriptionController.text,
|
||||||
|
'status': selectedStatus.value,
|
||||||
|
'images': List.from(selectedImages),
|
||||||
|
'items': List.from(packageItems),
|
||||||
|
'perJam': timeOptions['Per Jam']?.value ?? false,
|
||||||
|
'perHari': timeOptions['Per Hari']?.value ?? false,
|
||||||
|
'hargaJam': pricePerHourController.text,
|
||||||
|
'maxJam': maxHourController.text,
|
||||||
|
'hargaHari': pricePerDayController.text,
|
||||||
|
'maxHari': maxDayController.text,
|
||||||
|
};
|
||||||
|
isFormChanged.value = false;
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint('[ERROR] Gagal fetch paket detail: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pickImageFromCamera() async {
|
||||||
|
try {
|
||||||
|
final XFile? image = await _picker.pickImage(
|
||||||
|
source: ImageSource.camera,
|
||||||
|
imageQuality: 80,
|
||||||
|
maxWidth: 1024,
|
||||||
|
maxHeight: 1024,
|
||||||
|
);
|
||||||
|
if (image != null) {
|
||||||
|
selectedImages.add(image);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Gagal mengambil gambar dari kamera: $e',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pickImageFromGallery() async {
|
||||||
|
try {
|
||||||
|
final List<XFile>? images = await _picker.pickMultiImage(
|
||||||
|
imageQuality: 80,
|
||||||
|
maxWidth: 1024,
|
||||||
|
maxHeight: 1024,
|
||||||
|
);
|
||||||
|
if (images != null && images.isNotEmpty) {
|
||||||
|
for (final img in images) {
|
||||||
|
selectedImages.add(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Gagal memilih gambar dari galeri: $e',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkFormChanged() {
|
||||||
|
final current = {
|
||||||
|
'nama': nameController.text,
|
||||||
|
'deskripsi': descriptionController.text,
|
||||||
|
'status': selectedStatus.value,
|
||||||
|
'images': List.from(selectedImages),
|
||||||
|
'items': List.from(packageItems),
|
||||||
|
'perJam': timeOptions['Per Jam']?.value ?? false,
|
||||||
|
'perHari': timeOptions['Per Hari']?.value ?? false,
|
||||||
|
'hargaJam': pricePerHourController.text,
|
||||||
|
'maxJam': maxHourController.text,
|
||||||
|
'hargaHari': pricePerDayController.text,
|
||||||
|
'maxHari': maxDayController.text,
|
||||||
|
};
|
||||||
|
isFormChanged.value = current.toString() != initialFormData.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../controllers/petugas_aset_controller.dart';
|
import '../controllers/petugas_aset_controller.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import '../../../theme/app_colors_petugas.dart';
|
import '../../../theme/app_colors_petugas.dart';
|
||||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||||
import '../widgets/petugas_side_navbar.dart';
|
import '../widgets/petugas_side_navbar.dart';
|
||||||
@ -23,26 +24,12 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
controller = Get.find<PetugasAsetController>();
|
controller = Get.find<PetugasAsetController>();
|
||||||
_tabController = TabController(length: 2, vsync: this);
|
// Initialize with default tab (sewa)
|
||||||
|
controller.changeTab(0);
|
||||||
// Listen to tab changes and update controller
|
|
||||||
_tabController.addListener(() {
|
|
||||||
if (!_tabController.indexIsChanging) {
|
|
||||||
controller.changeTab(_tabController.index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen to controller tab changes and update TabController
|
|
||||||
ever(controller.selectedTabIndex, (index) {
|
|
||||||
if (_tabController.index != index) {
|
|
||||||
_tabController.animateTo(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tabController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +69,7 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
|||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildSearchBar(),
|
_buildSearchBar(),
|
||||||
_buildTabBar(),
|
const SizedBox(height: 16),
|
||||||
Expanded(child: _buildAssetList()),
|
Expanded(child: _buildAssetList()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -93,7 +80,13 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_ASET),
|
onPressed: () {
|
||||||
|
// Navigate to PetugasTambahAsetView in add mode
|
||||||
|
Get.toNamed(
|
||||||
|
Routes.PETUGAS_TAMBAH_ASET,
|
||||||
|
arguments: {'isEditing': false, 'assetData': null},
|
||||||
|
);
|
||||||
|
},
|
||||||
backgroundColor: AppColorsPetugas.babyBlueBright,
|
backgroundColor: AppColorsPetugas.babyBlueBright,
|
||||||
icon: Icon(Icons.add, color: AppColorsPetugas.blueGrotto),
|
icon: Icon(Icons.add, color: AppColorsPetugas.blueGrotto),
|
||||||
label: Text(
|
label: Text(
|
||||||
@ -144,60 +137,19 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTabBar() {
|
// Tab bar has been removed as per requirements
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColorsPetugas.babyBlueLight,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: TabBar(
|
|
||||||
controller: _tabController,
|
|
||||||
labelColor: Colors.white,
|
|
||||||
unselectedLabelColor: AppColorsPetugas.textSecondary,
|
|
||||||
indicatorSize: TabBarIndicatorSize.tab,
|
|
||||||
indicator: BoxDecoration(
|
|
||||||
color: AppColorsPetugas.blueGrotto,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
tabs: const [
|
|
||||||
Tab(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.shopping_cart, size: 18),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('Sewa', style: TextStyle(fontWeight: FontWeight.w600)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Tab(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.subscriptions, size: 18),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'Langganan',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAssetList() {
|
Widget _buildAssetList() {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
|
debugPrint('_buildAssetList: isLoading=${controller.isLoading.value}');
|
||||||
|
debugPrint(
|
||||||
|
'_buildAssetList: filteredAsetList length=${controller.filteredAsetList.length}',
|
||||||
|
);
|
||||||
|
if (controller.filteredAsetList.isNotEmpty) {
|
||||||
|
debugPrint(
|
||||||
|
'_buildAssetList: First item name=${controller.filteredAsetList[0]['nama']}',
|
||||||
|
);
|
||||||
|
}
|
||||||
if (controller.isLoading.value) {
|
if (controller.isLoading.value) {
|
||||||
return Center(
|
return Center(
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
@ -255,10 +207,15 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
|||||||
color: AppColorsPetugas.blueGrotto,
|
color: AppColorsPetugas.blueGrotto,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: controller.filteredAsetList.length,
|
itemCount: controller.filteredAsetList.length + 1,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
if (index < controller.filteredAsetList.length) {
|
||||||
final aset = controller.filteredAsetList[index];
|
final aset = controller.filteredAsetList[index];
|
||||||
return _buildAssetCard(context, aset);
|
return _buildAssetCard(context, aset);
|
||||||
|
} else {
|
||||||
|
// Blank space at the end
|
||||||
|
return const SizedBox(height: 80);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -266,7 +223,31 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAssetCard(BuildContext context, Map<String, dynamic> aset) {
|
Widget _buildAssetCard(BuildContext context, Map<String, dynamic> aset) {
|
||||||
final isAvailable = aset['tersedia'] == true;
|
debugPrint('\n--- Building Asset Card ---');
|
||||||
|
debugPrint('Asset data: $aset');
|
||||||
|
|
||||||
|
// Extract and validate all asset properties with proper null safety
|
||||||
|
final status =
|
||||||
|
aset['status']?.toString().toLowerCase() ?? 'tidak_diketahui';
|
||||||
|
final isAvailable = status == 'tersedia';
|
||||||
|
final imageUrl = aset['imageUrl']?.toString() ?? '';
|
||||||
|
final harga =
|
||||||
|
aset['harga'] is int
|
||||||
|
? aset['harga'] as int
|
||||||
|
: (int.tryParse(aset['harga']?.toString() ?? '0') ?? 0);
|
||||||
|
final satuanWaktu =
|
||||||
|
aset['satuan_waktu']?.toString().capitalizeFirst ?? 'Hari';
|
||||||
|
final nama = aset['nama']?.toString().trim() ?? 'Nama tidak tersedia';
|
||||||
|
final kategori = aset['kategori']?.toString().trim() ?? 'Umum';
|
||||||
|
final orderId = aset['order_id']?.toString() ?? '';
|
||||||
|
|
||||||
|
// Debug prints for development
|
||||||
|
debugPrint('Image URL: $imageUrl');
|
||||||
|
debugPrint('Harga: $harga');
|
||||||
|
debugPrint('Satuan Waktu: $satuanWaktu');
|
||||||
|
debugPrint('Nama: $nama');
|
||||||
|
debugPrint('Kategori: $kategori');
|
||||||
|
debugPrint('Status: $status (Available: $isAvailable)');
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
@ -290,24 +271,49 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Asset image
|
// Asset image
|
||||||
Container(
|
SizedBox(
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
child: ClipRRect(
|
||||||
color: AppColorsPetugas.babyBlueLight,
|
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(12),
|
topLeft: Radius.circular(12),
|
||||||
bottomLeft: Radius.circular(12),
|
bottomLeft: Radius.circular(12),
|
||||||
),
|
),
|
||||||
),
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder:
|
||||||
|
(context, url) => Container(
|
||||||
|
color: AppColorsPetugas.babyBlueLight,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
_getAssetIcon(aset['kategori']),
|
_getAssetIcon(
|
||||||
color: AppColorsPetugas.navyBlue,
|
kategori,
|
||||||
|
), // Show category icon as placeholder
|
||||||
|
color: AppColorsPetugas.navyBlue.withOpacity(
|
||||||
|
0.5,
|
||||||
|
),
|
||||||
size: 32,
|
size: 32,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
errorWidget:
|
||||||
|
(context, url, error) => Container(
|
||||||
|
color: AppColorsPetugas.babyBlueLight,
|
||||||
|
child: Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons
|
||||||
|
.broken_image, // Or your preferred error icon
|
||||||
|
color: AppColorsPetugas.navyBlue.withOpacity(
|
||||||
|
0.5,
|
||||||
|
),
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Asset info
|
// Asset info
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -323,8 +329,8 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
aset['nama'],
|
nama,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: AppColorsPetugas.navyBlue,
|
color: AppColorsPetugas.navyBlue,
|
||||||
@ -333,12 +339,63 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
// Harga dan satuan waktu (multi-line, tampilkan semua dari satuanWaktuSewa)
|
||||||
'${controller.formatPrice(aset['harga'])} ${aset['satuan']}',
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final satuanWaktuList =
|
||||||
|
(aset['satuanWaktuSewa'] is List)
|
||||||
|
? List<Map<String, dynamic>>.from(
|
||||||
|
aset['satuanWaktuSewa'],
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
final validSatuanWaktu =
|
||||||
|
satuanWaktuList
|
||||||
|
.where(
|
||||||
|
(sw) =>
|
||||||
|
(sw['harga'] ?? 0) > 0 &&
|
||||||
|
(sw['nama_satuan_waktu'] !=
|
||||||
|
null &&
|
||||||
|
(sw['nama_satuan_waktu']
|
||||||
|
as String)
|
||||||
|
.isNotEmpty),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (validSatuanWaktu.isNotEmpty) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children:
|
||||||
|
validSatuanWaktu.map((sw) {
|
||||||
|
final harga = sw['harga'] ?? 0;
|
||||||
|
final satuan =
|
||||||
|
sw['nama_satuan_waktu'] ?? '';
|
||||||
|
return Text(
|
||||||
|
'${controller.formatPrice(harga)} / $satuan',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color:
|
||||||
|
AppColorsPetugas
|
||||||
|
.textSecondary,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// fallback: harga tunggal
|
||||||
|
return Text(
|
||||||
|
'${controller.formatPrice(aset['harga'] ?? 0)} / ${aset['satuan_waktu']?.toString().capitalizeFirst ?? 'Hari'}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColorsPetugas.textSecondary,
|
color: AppColorsPetugas.textSecondary,
|
||||||
),
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -383,11 +440,36 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
|||||||
children: [
|
children: [
|
||||||
// Edit icon
|
// Edit icon
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap:
|
onTap: () {
|
||||||
() => _showAddEditAssetDialog(
|
// Navigate to PetugasTambahAsetView in edit mode with only the asset ID
|
||||||
context,
|
final assetId =
|
||||||
aset: aset,
|
aset['id']?.toString() ??
|
||||||
),
|
''; // Changed from 'id_aset' to 'id'
|
||||||
|
debugPrint(
|
||||||
|
'[DEBUG] Navigating to edit asset with ID: $assetId',
|
||||||
|
);
|
||||||
|
debugPrint(
|
||||||
|
'[DEBUG] Full asset data: $aset',
|
||||||
|
); // Log full asset data for debugging
|
||||||
|
|
||||||
|
if (assetId.isEmpty) {
|
||||||
|
debugPrint('[ERROR] Asset ID is empty!');
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'ID Aset tidak valid',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Get.toNamed(
|
||||||
|
Routes.PETUGAS_TAMBAH_ASET,
|
||||||
|
arguments: {
|
||||||
|
'isEditing': true,
|
||||||
|
'assetId': assetId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(5),
|
padding: const EdgeInsets.all(5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
|||||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||||
import '../widgets/petugas_side_navbar.dart';
|
import '../widgets/petugas_side_navbar.dart';
|
||||||
import '../../../theme/app_colors_petugas.dart';
|
import '../../../theme/app_colors_petugas.dart';
|
||||||
|
import '../../../utils/format_utils.dart';
|
||||||
|
|
||||||
class PetugasBumdesDashboardView
|
class PetugasBumdesDashboardView
|
||||||
extends GetView<PetugasBumdesDashboardController> {
|
extends GetView<PetugasBumdesDashboardController> {
|
||||||
@ -23,12 +24,7 @@ class PetugasBumdesDashboardView
|
|||||||
backgroundColor: AppColorsPetugas.navyBlue,
|
backgroundColor: AppColorsPetugas.navyBlue,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
actions: [
|
// actions: [],
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.logout),
|
|
||||||
onPressed: () => _showLogoutConfirmation(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
drawer: PetugasSideNavbar(controller: controller),
|
drawer: PetugasSideNavbar(controller: controller),
|
||||||
drawerEdgeDragWidth: 60,
|
drawerEdgeDragWidth: 60,
|
||||||
@ -118,8 +114,6 @@ class PetugasBumdesDashboardView
|
|||||||
),
|
),
|
||||||
_buildRevenueStatistics(),
|
_buildRevenueStatistics(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildRevenueSources(),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildRevenueTrend(),
|
_buildRevenueTrend(),
|
||||||
|
|
||||||
// Add some padding at the bottom for better scrolling
|
// Add some padding at the bottom for better scrolling
|
||||||
@ -156,7 +150,31 @@ class PetugasBumdesDashboardView
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Obx(() {
|
||||||
|
final avatar = controller.avatarUrl.value;
|
||||||
|
if (avatar.isNotEmpty) {
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Image.network(
|
||||||
|
avatar,
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder:
|
||||||
|
(context, error, stackTrace) => Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.person,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 30,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.2),
|
color: Colors.white.withOpacity(0.2),
|
||||||
@ -174,7 +192,9 @@ class PetugasBumdesDashboardView
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 30,
|
size: 30,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -208,15 +228,17 @@ class PetugasBumdesDashboardView
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Obx(
|
Obx(() {
|
||||||
() => Text(
|
final name = controller.userName.value;
|
||||||
controller.userEmail.value,
|
final email = controller.userEmail.value;
|
||||||
|
return Text(
|
||||||
|
name.isNotEmpty ? name : email,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.white70,
|
color: Colors.white70,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -642,19 +664,24 @@ class PetugasBumdesDashboardView
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Obx(
|
Obx(() {
|
||||||
() => Text(
|
final stats = controller.pembayaranStats;
|
||||||
controller.totalPendapatanBulanIni.value,
|
final total = stats['totalThisMonth'] ?? 0.0;
|
||||||
|
return Text(
|
||||||
|
formatRupiah(total),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: AppColorsPetugas.success,
|
color: AppColorsPetugas.success,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
}),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Obx(
|
Obx(() {
|
||||||
() => Row(
|
final stats = controller.pembayaranStats;
|
||||||
|
final percent = stats['percentComparedLast'] ?? 0.0;
|
||||||
|
final isPositive = percent >= 0;
|
||||||
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@ -663,7 +690,7 @@ class PetugasBumdesDashboardView
|
|||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color:
|
color:
|
||||||
controller.isKenaikanPositif.value
|
isPositive
|
||||||
? AppColorsPetugas.success.withOpacity(
|
? AppColorsPetugas.success.withOpacity(
|
||||||
0.1,
|
0.1,
|
||||||
)
|
)
|
||||||
@ -676,23 +703,23 @@ class PetugasBumdesDashboardView
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
controller.isKenaikanPositif.value
|
isPositive
|
||||||
? Icons.arrow_upward
|
? Icons.arrow_upward
|
||||||
: Icons.arrow_downward,
|
: Icons.arrow_downward,
|
||||||
size: 14,
|
size: 14,
|
||||||
color:
|
color:
|
||||||
controller.isKenaikanPositif.value
|
isPositive
|
||||||
? AppColorsPetugas.success
|
? AppColorsPetugas.success
|
||||||
: AppColorsPetugas.error,
|
: AppColorsPetugas.error,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
controller.persentaseKenaikan.value,
|
'${percent.toStringAsFixed(1)}%',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color:
|
color:
|
||||||
controller.isKenaikanPositif.value
|
isPositive
|
||||||
? AppColorsPetugas.success
|
? AppColorsPetugas.success
|
||||||
: AppColorsPetugas.error,
|
: AppColorsPetugas.error,
|
||||||
),
|
),
|
||||||
@ -709,8 +736,8 @@ class PetugasBumdesDashboardView
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -747,12 +774,29 @@ class PetugasBumdesDashboardView
|
|||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildRevenueQuickInfo(
|
child: Obx(() {
|
||||||
'Pendapatan Sewa',
|
final stats = controller.pembayaranStats;
|
||||||
controller.pendapatanSewa.value,
|
final totalTunai = stats['totalTunai'] ?? 0.0;
|
||||||
|
return _buildRevenueQuickInfo(
|
||||||
|
'Tunai',
|
||||||
|
formatRupiah(totalTunai),
|
||||||
AppColorsPetugas.navyBlue,
|
AppColorsPetugas.navyBlue,
|
||||||
Icons.shopping_cart_outlined,
|
Icons.payments,
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Obx(() {
|
||||||
|
final stats = controller.pembayaranStats;
|
||||||
|
final totalTransfer = stats['totalTransfer'] ?? 0.0;
|
||||||
|
return _buildRevenueQuickInfo(
|
||||||
|
'Transfer',
|
||||||
|
formatRupiah(totalTransfer),
|
||||||
|
AppColorsPetugas.blueGrotto,
|
||||||
|
Icons.account_balance,
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -811,81 +855,6 @@ class PetugasBumdesDashboardView
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRevenueSources() {
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
shadowColor: AppColorsPetugas.shadowColor,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Sumber Pendapatan',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColorsPetugas.navyBlue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
// Revenue Donut Chart
|
|
||||||
Expanded(
|
|
||||||
flex: 2,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColorsPetugas.navyBlue.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Sewa Aset',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColorsPetugas.navyBlue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Obx(
|
|
||||||
() => Text(
|
|
||||||
controller.pendapatanSewa.value,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColorsPetugas.navyBlue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'100% dari total pendapatan',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRevenueTrend() {
|
Widget _buildRevenueTrend() {
|
||||||
final months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun'];
|
final months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun'];
|
||||||
|
|
||||||
@ -912,6 +881,9 @@ class PetugasBumdesDashboardView
|
|||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
// Get the trend data from controller
|
// Get the trend data from controller
|
||||||
final List<double> trendData = controller.trendPendapatan;
|
final List<double> trendData = controller.trendPendapatan;
|
||||||
|
if (trendData.isEmpty) {
|
||||||
|
return Center(child: Text('Tidak ada data'));
|
||||||
|
}
|
||||||
final double maxValue = trendData.reduce(
|
final double maxValue = trendData.reduce(
|
||||||
(curr, next) => curr > next ? curr : next,
|
(curr, next) => curr > next ? curr : next,
|
||||||
);
|
);
|
||||||
@ -925,28 +897,28 @@ class PetugasBumdesDashboardView
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${maxValue.toStringAsFixed(1)}M',
|
formatRupiah(maxValue),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: AppColorsPetugas.textSecondary,
|
color: AppColorsPetugas.textSecondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${(maxValue * 0.75).toStringAsFixed(1)}M',
|
formatRupiah(maxValue * 0.75),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: AppColorsPetugas.textSecondary,
|
color: AppColorsPetugas.textSecondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${(maxValue * 0.5).toStringAsFixed(1)}M',
|
formatRupiah(maxValue * 0.5),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: AppColorsPetugas.textSecondary,
|
color: AppColorsPetugas.textSecondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${(maxValue * 0.25).toStringAsFixed(1)}M',
|
formatRupiah(maxValue * 0.25),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: AppColorsPetugas.textSecondary,
|
color: AppColorsPetugas.textSecondary,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../controllers/petugas_paket_controller.dart';
|
import 'package:bumrent_app/app/modules/petugas_bumdes/controllers/petugas_paket_controller.dart';
|
||||||
import '../../../theme/app_colors_petugas.dart';
|
import 'package:bumrent_app/app/routes/app_pages.dart';
|
||||||
|
import 'package:bumrent_app/app/data/models/paket_model.dart';
|
||||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||||
import '../widgets/petugas_side_navbar.dart';
|
import '../widgets/petugas_side_navbar.dart';
|
||||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||||
import '../../../routes/app_routes.dart';
|
import '../../../routes/app_routes.dart';
|
||||||
|
import '../../../theme/app_colors_petugas.dart';
|
||||||
|
|
||||||
class PetugasPaketView extends GetView<PetugasPaketController> {
|
class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||||
const PetugasPaketView({Key? key}) : super(key: key);
|
const PetugasPaketView({Key? key}) : super(key: key);
|
||||||
@ -53,7 +55,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET),
|
onPressed:
|
||||||
|
() => Get.toNamed(
|
||||||
|
Routes.PETUGAS_TAMBAH_PAKET,
|
||||||
|
arguments: {'isEditing': false},
|
||||||
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
'Tambah Paket',
|
'Tambah Paket',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@ -115,7 +121,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controller.filteredPaketList.isEmpty) {
|
if (controller.filteredPackages.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@ -136,7 +142,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET),
|
onPressed:
|
||||||
|
() => Get.toNamed(
|
||||||
|
Routes.PETUGAS_TAMBAH_PAKET,
|
||||||
|
arguments: {'isEditing': false},
|
||||||
|
),
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: const Text('Tambah Paket'),
|
label: const Text('Tambah Paket'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@ -161,18 +171,192 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
color: AppColorsPetugas.blueGrotto,
|
color: AppColorsPetugas.blueGrotto,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: controller.filteredPaketList.length,
|
itemCount: controller.filteredPackages.length + 1,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final paket = controller.filteredPaketList[index];
|
if (index < controller.filteredPackages.length) {
|
||||||
|
final paket = controller.filteredPackages[index];
|
||||||
return _buildPaketCard(context, paket);
|
return _buildPaketCard(context, paket);
|
||||||
|
} else {
|
||||||
|
// Blank space at the end
|
||||||
|
return const SizedBox(height: 80);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPaketCard(BuildContext context, Map<String, dynamic> paket) {
|
// Format price helper method
|
||||||
final isAvailable = paket['tersedia'] == true;
|
String _formatPrice(dynamic price) {
|
||||||
|
if (price == null) return '0';
|
||||||
|
// If price is a string that can be parsed to a number
|
||||||
|
if (price is String) {
|
||||||
|
final number = double.tryParse(price) ?? 0;
|
||||||
|
return number
|
||||||
|
.toStringAsFixed(0)
|
||||||
|
.replaceAllMapped(
|
||||||
|
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||||
|
(Match m) => '${m[1]}.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// If price is already a number
|
||||||
|
if (price is num) {
|
||||||
|
return price
|
||||||
|
.toStringAsFixed(0)
|
||||||
|
.replaceAllMapped(
|
||||||
|
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||||
|
(Match m) => '${m[1]}.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to get time unit name based on ID
|
||||||
|
String _getTimeUnitName(dynamic unitId) {
|
||||||
|
if (unitId == null) return 'unit';
|
||||||
|
|
||||||
|
// Convert to string in case it's not already
|
||||||
|
final unitIdStr = unitId.toString().toLowerCase();
|
||||||
|
|
||||||
|
// Map of known time unit IDs to their display names
|
||||||
|
final timeUnitMap = {
|
||||||
|
'6eaa32d9-855d-4214-b5b5-5c73d3edd9c5': 'jam',
|
||||||
|
'582b7e66-6869-4495-9856-cef4a46683b0': 'hari',
|
||||||
|
// Add more mappings as needed
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the unitId is a known ID, return the corresponding name
|
||||||
|
if (timeUnitMap.containsKey(unitIdStr)) {
|
||||||
|
return timeUnitMap[unitIdStr]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the unit is already a name (like 'jam' or 'hari')
|
||||||
|
final knownUnits = ['jam', 'hari', 'minggu', 'bulan'];
|
||||||
|
if (knownUnits.contains(unitIdStr)) {
|
||||||
|
return unitIdStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the unit is a Map, try to extract the name from common fields
|
||||||
|
if (unitId is Map) {
|
||||||
|
return unitId['nama']?.toString().toLowerCase() ??
|
||||||
|
unitId['name']?.toString().toLowerCase() ??
|
||||||
|
unitId['satuan_waktu']?.toString().toLowerCase() ??
|
||||||
|
'unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default fallback
|
||||||
|
return 'unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to log time unit details
|
||||||
|
void _logTimeUnitDetails(
|
||||||
|
String packageName,
|
||||||
|
List<Map<String, dynamic>> timeUnits,
|
||||||
|
) {
|
||||||
|
debugPrint('\n📦 [DEBUG] Package: $packageName');
|
||||||
|
debugPrint('🔄 Found ${timeUnits.length} time units:');
|
||||||
|
|
||||||
|
for (var i = 0; i < timeUnits.length; i++) {
|
||||||
|
final unit = timeUnits[i];
|
||||||
|
debugPrint('\n ⏱️ Time Unit #${i + 1}:');
|
||||||
|
|
||||||
|
// Log all available keys and values
|
||||||
|
debugPrint(' ├─ All fields: $unit');
|
||||||
|
|
||||||
|
// Log specific fields we're interested in
|
||||||
|
unit.forEach((key, value) {
|
||||||
|
debugPrint(' ├─ $key: $value (${value.runtimeType})');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Special handling for satuan_waktu if it's a map
|
||||||
|
if (unit['satuan_waktu'] is Map) {
|
||||||
|
final satuanWaktu = unit['satuan_waktu'] as Map;
|
||||||
|
debugPrint(' └─ satuan_waktu details:');
|
||||||
|
satuanWaktu.forEach((k, v) {
|
||||||
|
debugPrint(' ├─ $k: $v (${v.runtimeType})');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugPrint('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPaketCard(BuildContext context, dynamic paket) {
|
||||||
|
// Handle both Map and PaketModel for backward compatibility
|
||||||
|
final isPaketModel = paket is PaketModel;
|
||||||
|
|
||||||
|
debugPrint('\n🔍 [_buildPaketCard] Paket type: ${paket.runtimeType}');
|
||||||
|
debugPrint('📋 Paket data: $paket');
|
||||||
|
|
||||||
|
// Extract status based on type
|
||||||
|
final String status =
|
||||||
|
isPaketModel
|
||||||
|
? (paket.status?.toString().capitalizeFirst ?? 'Tidak Diketahui')
|
||||||
|
: (paket['status']?.toString().capitalizeFirst ??
|
||||||
|
'Tidak Diketahui');
|
||||||
|
|
||||||
|
debugPrint('🏷️ Extracted status: $status (isPaketModel: $isPaketModel)');
|
||||||
|
|
||||||
|
// Extract availability based on type
|
||||||
|
final bool isAvailable =
|
||||||
|
isPaketModel
|
||||||
|
? (paket.kuantitas > 0)
|
||||||
|
: ((paket['kuantitas'] as int?) ?? 0) > 0;
|
||||||
|
|
||||||
|
final String nama =
|
||||||
|
isPaketModel
|
||||||
|
? paket.nama
|
||||||
|
: (paket['nama']?.toString() ?? 'Paket Tanpa Nama');
|
||||||
|
|
||||||
|
// Debug package info
|
||||||
|
debugPrint('\n📦 [PACKAGE] ${paket.runtimeType} - $nama');
|
||||||
|
debugPrint('├─ isPaketModel: $isPaketModel');
|
||||||
|
debugPrint('├─ Available: $isAvailable');
|
||||||
|
|
||||||
|
// Get the first rental time unit price if available, otherwise use the base price
|
||||||
|
final dynamic harga;
|
||||||
|
if (isPaketModel) {
|
||||||
|
if (paket.satuanWaktuSewa.isNotEmpty) {
|
||||||
|
_logTimeUnitDetails(nama, paket.satuanWaktuSewa);
|
||||||
|
|
||||||
|
// Get the first time unit with its price
|
||||||
|
final firstUnit = paket.satuanWaktuSewa.first;
|
||||||
|
final firstUnitPrice = firstUnit['harga'];
|
||||||
|
|
||||||
|
debugPrint('💰 First time unit price: $firstUnitPrice');
|
||||||
|
debugPrint('⏱️ First time unit ID: ${firstUnit['satuan_waktu_id']}');
|
||||||
|
debugPrint('📝 First time unit details: $firstUnit');
|
||||||
|
|
||||||
|
// Always use the first time unit's price if available
|
||||||
|
harga = firstUnitPrice ?? 0;
|
||||||
|
} else {
|
||||||
|
debugPrint('⚠️ No time units found for package: $nama');
|
||||||
|
debugPrint('ℹ️ Using base price: ${paket.harga}');
|
||||||
|
harga = paket.harga;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For non-PaketModel (Map) data
|
||||||
|
if (isPaketModel && paket.satuanWaktuSewa.isNotEmpty) {
|
||||||
|
final firstUnit = paket.satuanWaktuSewa.first;
|
||||||
|
final firstUnitPrice = firstUnit['harga'];
|
||||||
|
debugPrint('💰 [MAP] First time unit price: $firstUnitPrice');
|
||||||
|
harga = firstUnitPrice ?? 0;
|
||||||
|
} else {
|
||||||
|
debugPrint('⚠️ [MAP] No time units found for package: $nama');
|
||||||
|
debugPrint('ℹ️ [MAP] Using base price: ${paket['harga']}');
|
||||||
|
harga = paket['harga'] ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('💵 Final price being used: $harga\n');
|
||||||
|
|
||||||
|
// Get the main photo URL
|
||||||
|
final String? foto =
|
||||||
|
isPaketModel
|
||||||
|
? (paket.images?.isNotEmpty == true
|
||||||
|
? paket.images!.first
|
||||||
|
: paket.foto_paket)
|
||||||
|
: (paket['foto_paket']?.toString() ??
|
||||||
|
(paket['foto'] is String ? paket['foto'] : null));
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
@ -196,24 +380,85 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Paket image or icon
|
// Paket image or icon
|
||||||
Container(
|
SizedBox(
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
child: ClipRRect(
|
||||||
color: AppColorsPetugas.babyBlueLight,
|
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(12),
|
topLeft: Radius.circular(12),
|
||||||
bottomLeft: Radius.circular(12),
|
bottomLeft: Radius.circular(12),
|
||||||
),
|
),
|
||||||
),
|
child:
|
||||||
|
foto != null && foto.isNotEmpty
|
||||||
|
? Image.network(
|
||||||
|
foto,
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder:
|
||||||
|
(context, error, stackTrace) => Container(
|
||||||
|
color: AppColorsPetugas.babyBlueLight,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
_getPaketIcon(paket['kategori']),
|
_getPaketIcon(
|
||||||
color: AppColorsPetugas.navyBlue,
|
_getTimeUnitName(
|
||||||
|
isPaketModel
|
||||||
|
? (paket
|
||||||
|
.satuanWaktuSewa
|
||||||
|
.isNotEmpty
|
||||||
|
? paket
|
||||||
|
.satuanWaktuSewa
|
||||||
|
.first['satuan_waktu_id'] ??
|
||||||
|
'hari'
|
||||||
|
: 'hari')
|
||||||
|
: (paket['satuanWaktuSewa'] !=
|
||||||
|
null &&
|
||||||
|
paket['satuanWaktuSewa']
|
||||||
|
.isNotEmpty
|
||||||
|
? paket['satuanWaktuSewa'][0]['satuan_waktu_id']
|
||||||
|
?.toString() ??
|
||||||
|
'hari'
|
||||||
|
: 'hari'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
color: AppColorsPetugas.navyBlue
|
||||||
|
.withOpacity(0.5),
|
||||||
size: 32,
|
size: 32,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
color: AppColorsPetugas.babyBlueLight,
|
||||||
|
child: Center(
|
||||||
|
child: Icon(
|
||||||
|
_getPaketIcon(
|
||||||
|
_getTimeUnitName(
|
||||||
|
isPaketModel
|
||||||
|
? (paket.satuanWaktuSewa.isNotEmpty
|
||||||
|
? paket
|
||||||
|
.satuanWaktuSewa
|
||||||
|
.first['satuan_waktu_id'] ??
|
||||||
|
'hari'
|
||||||
|
: 'hari')
|
||||||
|
: (paket['satuanWaktuSewa'] != null &&
|
||||||
|
paket['satuanWaktuSewa']
|
||||||
|
.isNotEmpty
|
||||||
|
? paket['satuanWaktuSewa'][0]['satuan_waktu_id']
|
||||||
|
?.toString() ??
|
||||||
|
'hari'
|
||||||
|
: 'hari'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
color: AppColorsPetugas.navyBlue.withOpacity(
|
||||||
|
0.5,
|
||||||
|
),
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Paket info
|
// Paket info
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -228,9 +473,10 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
// Package name
|
||||||
Text(
|
Text(
|
||||||
paket['nama'],
|
nama,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: AppColorsPetugas.navyBlue,
|
color: AppColorsPetugas.navyBlue,
|
||||||
@ -239,6 +485,111 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
// Prices with time units
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final List<Map<String, dynamic>> timeUnits =
|
||||||
|
[];
|
||||||
|
|
||||||
|
// Get all time units
|
||||||
|
if (isPaketModel &&
|
||||||
|
paket.satuanWaktuSewa.isNotEmpty) {
|
||||||
|
timeUnits.addAll(paket.satuanWaktuSewa);
|
||||||
|
} else if (!isPaketModel &&
|
||||||
|
paket['satuanWaktuSewa'] != null &&
|
||||||
|
paket['satuanWaktuSewa'].isNotEmpty) {
|
||||||
|
timeUnits.addAll(
|
||||||
|
List<Map<String, dynamic>>.from(
|
||||||
|
paket['satuanWaktuSewa'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no time units, show nothing
|
||||||
|
if (timeUnits.isEmpty)
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
|
||||||
|
// Filter out time units with price 0 or null
|
||||||
|
final validTimeUnits =
|
||||||
|
timeUnits.where((unit) {
|
||||||
|
final price =
|
||||||
|
unit['harga'] is int
|
||||||
|
? unit['harga']
|
||||||
|
: int.tryParse(
|
||||||
|
unit['harga']
|
||||||
|
?.toString() ??
|
||||||
|
'0',
|
||||||
|
) ??
|
||||||
|
0;
|
||||||
|
return price > 0;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (validTimeUnits.isEmpty)
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children:
|
||||||
|
validTimeUnits
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final unit = entry.value;
|
||||||
|
final unitPrice =
|
||||||
|
unit['harga'] is int
|
||||||
|
? unit['harga']
|
||||||
|
: int.tryParse(
|
||||||
|
unit['harga']
|
||||||
|
?.toString() ??
|
||||||
|
'0',
|
||||||
|
) ??
|
||||||
|
0;
|
||||||
|
final unitName = _getTimeUnitName(
|
||||||
|
unit['satuan_waktu_id'],
|
||||||
|
);
|
||||||
|
final isFirst = index == 0;
|
||||||
|
|
||||||
|
if (unitPrice <= 0)
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
'Rp ${_formatPrice(unitPrice)}/$unitName',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color:
|
||||||
|
AppColorsPetugas
|
||||||
|
.textSecondary,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow:
|
||||||
|
TextOverflow.ellipsis,
|
||||||
|
softWrap: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
(widget) => widget is! SizedBox,
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!isPaketModel &&
|
||||||
|
paket['harga'] != null &&
|
||||||
|
(paket['harga'] is int
|
||||||
|
? paket['harga']
|
||||||
|
: int.tryParse(
|
||||||
|
paket['harga']?.toString() ??
|
||||||
|
'0',
|
||||||
|
) ??
|
||||||
|
0) >
|
||||||
|
0) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Rp ${_formatPrice(paket['harga'])}',
|
'Rp ${_formatPrice(paket['harga'])}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@ -247,6 +598,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -258,25 +610,31 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color:
|
color:
|
||||||
isAvailable
|
status.toLowerCase() == 'tersedia'
|
||||||
? AppColorsPetugas.successLight
|
? AppColorsPetugas.successLight
|
||||||
|
: status.toLowerCase() == 'pemeliharaan'
|
||||||
|
? AppColorsPetugas.warningLight
|
||||||
: AppColorsPetugas.errorLight,
|
: AppColorsPetugas.errorLight,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color:
|
color:
|
||||||
isAvailable
|
status.toLowerCase() == 'tersedia'
|
||||||
? AppColorsPetugas.success
|
? AppColorsPetugas.success
|
||||||
|
: status.toLowerCase() == 'pemeliharaan'
|
||||||
|
? AppColorsPetugas.warning
|
||||||
: AppColorsPetugas.error,
|
: AppColorsPetugas.error,
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
isAvailable ? 'Aktif' : 'Nonaktif',
|
status,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color:
|
color:
|
||||||
isAvailable
|
status.toLowerCase() == 'tersedia'
|
||||||
? AppColorsPetugas.success
|
? AppColorsPetugas.success
|
||||||
|
: status.toLowerCase() == 'pemeliharaan'
|
||||||
|
? AppColorsPetugas.warning
|
||||||
: AppColorsPetugas.error,
|
: AppColorsPetugas.error,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
@ -290,9 +648,12 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
// Edit icon
|
// Edit icon
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap:
|
onTap:
|
||||||
() => _showAddEditPaketDialog(
|
() => Get.toNamed(
|
||||||
context,
|
Routes.PETUGAS_TAMBAH_PAKET,
|
||||||
paket: paket,
|
arguments: {
|
||||||
|
'isEditing': true,
|
||||||
|
'paket': paket,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(5),
|
padding: const EdgeInsets.all(5),
|
||||||
@ -350,33 +711,42 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatPrice(dynamic price) {
|
// Add this helper method to get color based on status
|
||||||
if (price == null) return '0';
|
Color _getStatusColor(String status) {
|
||||||
|
switch (status.toLowerCase()) {
|
||||||
// Convert the price to string and handle formatting
|
case 'aktif':
|
||||||
String priceStr = price.toString();
|
return AppColorsPetugas.success;
|
||||||
|
case 'tidak aktif':
|
||||||
// Add thousand separators
|
case 'nonaktif':
|
||||||
final RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))');
|
return AppColorsPetugas.error;
|
||||||
String formatted = priceStr.replaceAllMapped(reg, (Match m) => '${m[1]}.');
|
case 'dalam perbaikan':
|
||||||
|
case 'maintenance':
|
||||||
return formatted;
|
return AppColorsPetugas.warning;
|
||||||
|
case 'tersedia':
|
||||||
|
return AppColorsPetugas.success;
|
||||||
|
case 'pemeliharaan':
|
||||||
|
return AppColorsPetugas.warning;
|
||||||
|
default:
|
||||||
|
return Colors.grey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconData _getPaketIcon(String? category) {
|
IconData _getPaketIcon(String? timeUnit) {
|
||||||
if (category == null) return Icons.category;
|
if (timeUnit == null) return Icons.access_time;
|
||||||
|
|
||||||
switch (category.toLowerCase()) {
|
switch (timeUnit.toLowerCase()) {
|
||||||
case 'bulanan':
|
case 'jam':
|
||||||
return Icons.calendar_month;
|
return Icons.access_time;
|
||||||
case 'tahunan':
|
case 'hari':
|
||||||
return Icons.calendar_today;
|
return Icons.calendar_today;
|
||||||
case 'premium':
|
case 'minggu':
|
||||||
return Icons.star;
|
return Icons.date_range;
|
||||||
case 'bisnis':
|
case 'bulan':
|
||||||
return Icons.business;
|
return Icons.calendar_month;
|
||||||
|
case 'tahun':
|
||||||
|
return Icons.calendar_view_month;
|
||||||
default:
|
default:
|
||||||
return Icons.category;
|
return Icons.access_time;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,7 +796,27 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showPaketDetails(BuildContext context, Map<String, dynamic> paket) {
|
void _showPaketDetails(BuildContext context, dynamic paket) {
|
||||||
|
// Handle both Map and PaketModel for backward compatibility
|
||||||
|
final isPaketModel = paket is PaketModel;
|
||||||
|
final String nama =
|
||||||
|
isPaketModel
|
||||||
|
? paket.nama
|
||||||
|
: (paket['nama']?.toString() ?? 'Paket Tanpa Nama');
|
||||||
|
final String? deskripsi =
|
||||||
|
isPaketModel ? paket.deskripsi : paket['deskripsi']?.toString();
|
||||||
|
final bool isAvailable =
|
||||||
|
isPaketModel
|
||||||
|
? (paket.kuantitas > 0)
|
||||||
|
: ((paket['kuantitas'] as int?) ?? 0) > 0;
|
||||||
|
final dynamic harga =
|
||||||
|
isPaketModel
|
||||||
|
? (paket.satuanWaktuSewa.isNotEmpty
|
||||||
|
? paket.satuanWaktuSewa.first['harga']
|
||||||
|
: paket.harga)
|
||||||
|
: (paket['harga'] ?? 0);
|
||||||
|
// Items are not part of the PaketModel, so we'll use an empty list
|
||||||
|
final List<Map<String, dynamic>> items = [];
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
@ -448,7 +838,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
paket['nama'],
|
nama,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -473,16 +863,15 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildDetailItem('Kategori', paket['kategori']),
|
|
||||||
_buildDetailItem(
|
_buildDetailItem(
|
||||||
'Harga',
|
'Harga',
|
||||||
controller.formatPrice(paket['harga']),
|
'Rp ${_formatPrice(harga)}',
|
||||||
),
|
),
|
||||||
_buildDetailItem(
|
_buildDetailItem(
|
||||||
'Status',
|
'Status',
|
||||||
paket['tersedia'] ? 'Tersedia' : 'Tidak Tersedia',
|
isAvailable ? 'Tersedia' : 'Tidak Tersedia',
|
||||||
),
|
),
|
||||||
_buildDetailItem('Deskripsi', paket['deskripsi']),
|
_buildDetailItem('Deskripsi', deskripsi ?? '-'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -502,11 +891,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: paket['items'].length,
|
itemCount: items.length,
|
||||||
separatorBuilder:
|
separatorBuilder:
|
||||||
(context, index) => const Divider(height: 1),
|
(context, index) => const Divider(height: 1),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = paket['items'][index];
|
final item = items[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor: AppColorsPetugas.babyBlue,
|
backgroundColor: AppColorsPetugas.babyBlue,
|
||||||
@ -601,10 +990,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showAddEditPaketDialog(
|
void _showAddEditPaketDialog(BuildContext context, {dynamic paket}) {
|
||||||
BuildContext context, {
|
// Handle both Map and PaketModel for backward compatibility
|
||||||
Map<String, dynamic>? paket,
|
final isPaketModel = paket is PaketModel;
|
||||||
}) {
|
final String? id = isPaketModel ? paket.id : paket?['id'];
|
||||||
|
final String title = id == null ? 'Tambah Paket' : 'Edit Paket';
|
||||||
final isEditing = paket != null;
|
final isEditing = paket != null;
|
||||||
|
|
||||||
// This would be implemented with proper form validation in a real app
|
// This would be implemented with proper form validation in a real app
|
||||||
@ -613,7 +1003,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(
|
title: Text(
|
||||||
isEditing ? 'Edit Paket' : 'Tambah Paket Baru',
|
title,
|
||||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||||
),
|
),
|
||||||
content: const Text(
|
content: const Text(
|
||||||
@ -652,10 +1042,13 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showDeleteConfirmation(
|
void _showDeleteConfirmation(BuildContext context, dynamic paket) {
|
||||||
BuildContext context,
|
// Handle both Map and PaketModel for backward compatibility
|
||||||
Map<String, dynamic> paket,
|
final isPaketModel = paket is PaketModel;
|
||||||
) {
|
final String id = isPaketModel ? paket.id : (paket['id']?.toString() ?? '');
|
||||||
|
final String nama =
|
||||||
|
isPaketModel ? paket.nama : (paket['nama']?.toString() ?? 'Paket');
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
@ -664,9 +1057,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
'Konfirmasi Hapus',
|
'Konfirmasi Hapus',
|
||||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||||
),
|
),
|
||||||
content: Text(
|
content: Text('Apakah Anda yakin ingin menghapus paket "$nama"?'),
|
||||||
'Apakah Anda yakin ingin menghapus paket "${paket['nama']}"?',
|
|
||||||
),
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
@ -678,7 +1069,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
controller.deletePaket(paket['id']);
|
controller.deletePaket(id);
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Paket Dihapus',
|
'Paket Dihapus',
|
||||||
'Paket berhasil dihapus dari sistem',
|
'Paket berhasil dihapus dari sistem',
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
|||||||
import '../widgets/petugas_side_navbar.dart';
|
import '../widgets/petugas_side_navbar.dart';
|
||||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||||
import 'petugas_detail_sewa_view.dart';
|
import 'petugas_detail_sewa_view.dart';
|
||||||
|
import '../../../data/models/rental_booking_model.dart';
|
||||||
|
|
||||||
class PetugasSewaView extends StatefulWidget {
|
class PetugasSewaView extends StatefulWidget {
|
||||||
const PetugasSewaView({Key? key}) : super(key: key);
|
const PetugasSewaView({Key? key}) : super(key: key);
|
||||||
@ -160,6 +161,10 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSearchSection() {
|
Widget _buildSearchSection() {
|
||||||
|
// Tambahkan controller untuk TextField agar bisa dikosongkan
|
||||||
|
final TextEditingController searchController = TextEditingController(
|
||||||
|
text: controller.searchQuery.value,
|
||||||
|
);
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -173,9 +178,9 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
controller: searchController,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
controller.setSearchQuery(value);
|
controller.setSearchQuery(value);
|
||||||
controller.setOrderIdQuery(value);
|
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Cari nama warga atau ID pesanan...',
|
hintText: 'Cari nama warga atau ID pesanan...',
|
||||||
@ -204,11 +209,22 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
),
|
),
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
suffixIcon: Icon(
|
suffixIcon: Obx(
|
||||||
Icons.tune_rounded,
|
() =>
|
||||||
|
controller.searchQuery.value.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.close,
|
||||||
color: AppColorsPetugas.textSecondary,
|
color: AppColorsPetugas.textSecondary,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
|
onPressed: () {
|
||||||
|
searchController.clear();
|
||||||
|
controller.setSearchQuery('');
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: SizedBox.shrink(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -241,17 +257,44 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
final filteredList =
|
final filteredList =
|
||||||
status == 'Semua'
|
status == 'Semua'
|
||||||
? controller.filteredSewaList
|
? controller.filteredSewaList
|
||||||
|
: status == 'Menunggu Pembayaran'
|
||||||
|
? controller.sewaList
|
||||||
|
.where(
|
||||||
|
(sewa) =>
|
||||||
|
sewa.status.toUpperCase() == 'MENUNGGU PEMBAYARAN' ||
|
||||||
|
sewa.status.toUpperCase() == 'PEMBAYARAN DENDA',
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
: status == 'Periksa Pembayaran'
|
: status == 'Periksa Pembayaran'
|
||||||
? controller.sewaList
|
? controller.sewaList
|
||||||
.where(
|
.where(
|
||||||
(sewa) =>
|
(sewa) =>
|
||||||
sewa['status'] == 'Periksa Pembayaran' ||
|
sewa.status.toUpperCase() == 'PERIKSA PEMBAYARAN' ||
|
||||||
sewa['status'] == 'Pembayaran Denda' ||
|
sewa.status.toUpperCase() == 'PERIKSA PEMBAYARAN DENDA',
|
||||||
sewa['status'] == 'Periksa Denda',
|
|
||||||
)
|
)
|
||||||
.toList()
|
.toList()
|
||||||
|
: status == 'Diterima'
|
||||||
|
? controller.sewaList
|
||||||
|
.where((sewa) => sewa.status.toUpperCase() == 'DITERIMA')
|
||||||
|
.toList()
|
||||||
|
: status == 'Aktif'
|
||||||
|
? controller.sewaList
|
||||||
|
.where((sewa) => sewa.status.toUpperCase() == 'AKTIF')
|
||||||
|
.toList()
|
||||||
|
: status == 'Dikembalikan'
|
||||||
|
? controller.sewaList
|
||||||
|
.where((sewa) => sewa.status.toUpperCase() == 'DIKEMBALIKAN')
|
||||||
|
.toList()
|
||||||
|
: status == 'Selesai'
|
||||||
|
? controller.sewaList
|
||||||
|
.where((sewa) => sewa.status.toUpperCase() == 'SELESAI')
|
||||||
|
.toList()
|
||||||
|
: status == 'Dibatalkan'
|
||||||
|
? controller.sewaList
|
||||||
|
.where((sewa) => sewa.status.toUpperCase() == 'DIBATALKAN')
|
||||||
|
.toList()
|
||||||
: controller.sewaList
|
: controller.sewaList
|
||||||
.where((sewa) => sewa['status'] == status)
|
.where((sewa) => sewa.status == status)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (filteredList.isEmpty) {
|
if (filteredList.isEmpty) {
|
||||||
@ -313,40 +356,25 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSewaCard(BuildContext context, Map<String, dynamic> sewa) {
|
Widget _buildSewaCard(BuildContext context, SewaModel sewa) {
|
||||||
final statusColor = controller.getStatusColor(sewa['status']);
|
final statusColor = controller.getStatusColor(sewa.status);
|
||||||
final status = sewa['status'];
|
final status = sewa.status;
|
||||||
|
|
||||||
// Get appropriate icon for status
|
// Get appropriate icon for status
|
||||||
IconData statusIcon;
|
IconData statusIcon = controller.getStatusIcon(status);
|
||||||
switch (status) {
|
|
||||||
case 'Menunggu Pembayaran':
|
// Flag untuk membedakan tipe pesanan
|
||||||
statusIcon = Icons.payments_outlined;
|
final bool isAset = sewa.tipePesanan == 'tunggal';
|
||||||
break;
|
final bool isPaket = sewa.tipePesanan == 'paket';
|
||||||
case 'Periksa Pembayaran':
|
|
||||||
statusIcon = Icons.fact_check_outlined;
|
// Pilih nama aset/paket
|
||||||
break;
|
final String namaAsetAtauPaket =
|
||||||
case 'Diterima':
|
isAset
|
||||||
statusIcon = Icons.check_circle_outlined;
|
? (sewa.asetNama ?? '-')
|
||||||
break;
|
: (isPaket ? (sewa.paketNama ?? '-') : '-');
|
||||||
case 'Pembayaran Denda':
|
// Pilih foto aset/paket jika ingin digunakan
|
||||||
statusIcon = Icons.money_off_csred_outlined;
|
final String? fotoAsetAtauPaket =
|
||||||
break;
|
isAset ? sewa.asetFoto : (isPaket ? sewa.paketFoto : null);
|
||||||
case 'Periksa Denda':
|
|
||||||
statusIcon = Icons.assignment_late_outlined;
|
|
||||||
break;
|
|
||||||
case 'Dikembalikan':
|
|
||||||
statusIcon = Icons.assignment_return_outlined;
|
|
||||||
break;
|
|
||||||
case 'Selesai':
|
|
||||||
statusIcon = Icons.task_alt_outlined;
|
|
||||||
break;
|
|
||||||
case 'Dibatalkan':
|
|
||||||
statusIcon = Icons.cancel_outlined;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
statusIcon = Icons.help_outline_rounded;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
@ -370,6 +398,35 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Status header inside the card
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: statusColor.withOpacity(0.12),
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(20),
|
||||||
|
topRight: Radius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(statusIcon, size: 16, color: statusColor),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
status,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: statusColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -378,14 +435,22 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 24,
|
radius: 24,
|
||||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||||
child: Text(
|
backgroundImage:
|
||||||
sewa['nama_warga'].substring(0, 1).toUpperCase(),
|
(sewa.wargaAvatar != null &&
|
||||||
|
sewa.wargaAvatar.isNotEmpty)
|
||||||
|
? NetworkImage(sewa.wargaAvatar)
|
||||||
|
: null,
|
||||||
|
child:
|
||||||
|
(sewa.wargaAvatar == null || sewa.wargaAvatar.isEmpty)
|
||||||
|
? Text(
|
||||||
|
sewa.wargaNama.substring(0, 1).toUpperCase(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: AppColorsPetugas.blueGrotto,
|
color: AppColorsPetugas.blueGrotto,
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
|
|
||||||
@ -395,58 +460,25 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
sewa['nama_warga'],
|
sewa.wargaNama,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: AppColorsPetugas.textPrimary,
|
color: AppColorsPetugas.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 3,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: statusColor.withOpacity(0.15),
|
|
||||||
borderRadius: BorderRadius.circular(30),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
statusIcon,
|
|
||||||
size: 12,
|
|
||||||
color: statusColor,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
Text(
|
||||||
status,
|
'Tanggal Pesan: ' +
|
||||||
|
(sewa.tanggalPemesanan != null
|
||||||
|
? '${sewa.tanggalPemesanan.day.toString().padLeft(2, '0')}-${sewa.tanggalPemesanan.month.toString().padLeft(2, '0')}-${sewa.tanggalPemesanan.year}'
|
||||||
|
: '-'),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: statusColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'#${sewa['order_id']}',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: AppColorsPetugas.textSecondary,
|
color: AppColorsPetugas.textSecondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Price
|
// Price
|
||||||
@ -460,7 +492,7 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
controller.formatPrice(sewa['total_biaya']),
|
controller.formatPrice(sewa.totalTagihan),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -481,12 +513,30 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
child: Divider(height: 1, color: Colors.grey.shade200),
|
child: Divider(height: 1, color: Colors.grey.shade200),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Asset details
|
// Asset/Paket details
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Asset icon
|
// Asset/Paket image or icon
|
||||||
|
if (fotoAsetAtauPaket != null &&
|
||||||
|
fotoAsetAtauPaket.isNotEmpty)
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Image.network(
|
||||||
|
fotoAsetAtauPaket,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder:
|
||||||
|
(context, error, stackTrace) => Icon(
|
||||||
|
Icons.inventory_2_outlined,
|
||||||
|
size: 28,
|
||||||
|
color: AppColorsPetugas.blueGrotto,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -501,13 +551,13 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
// Asset name and duration
|
// Asset/Paket name and duration
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
sewa['nama_aset'],
|
namaAsetAtauPaket,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@ -524,7 +574,7 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'${sewa['tanggal_mulai']} - ${sewa['tanggal_selesai']}',
|
'${sewa.waktuMulai.toIso8601String().substring(0, 10)} - ${sewa.waktuSelesai.toIso8601String().substring(0, 10)}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColorsPetugas.textSecondary,
|
color: AppColorsPetugas.textSecondary,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../../../theme/app_colors_petugas.dart';
|
import '../../../theme/app_colors_petugas.dart';
|
||||||
@ -9,32 +10,51 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return GestureDetector(
|
||||||
|
onTap: () => FocusScope.of(context).unfocus(),
|
||||||
|
child: Obx(() => Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text(
|
title: Text(
|
||||||
'Tambah Aset',
|
controller.isEditing.value ? 'Edit Aset' : 'Tambah Aset',
|
||||||
style: TextStyle(fontWeight: FontWeight.w600),
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
backgroundColor: AppColorsPetugas.navyBlue,
|
backgroundColor: AppColorsPetugas.navyBlue,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [_buildHeaderSection(), _buildFormSection(context)],
|
children: [
|
||||||
|
_buildHeaderSection(),
|
||||||
|
_buildFormSection(context),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (controller.isLoading.value)
|
||||||
|
Container(
|
||||||
|
color: Colors.black54,
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(AppColorsPetugas.blueGrotto),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
bottomNavigationBar: _buildBottomBar(),
|
bottomNavigationBar: _buildBottomBar(),
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeaderSection() {
|
Widget _buildHeaderSection() {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.only(top: 10, left: 20, right: 20, bottom: 5), // Reduced padding
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto],
|
colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto],
|
||||||
@ -42,50 +62,8 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
|||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Container(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
height: 12, // Further reduced height
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.2),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.inventory_2_outlined,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 28,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'Informasi Aset Baru',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'Isi data dengan lengkap untuk menambahkan aset',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.white.withOpacity(0.8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -131,49 +109,29 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
|||||||
_buildImageUploader(),
|
_buildImageUploader(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Category Section
|
// Status Section
|
||||||
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'),
|
_buildSectionHeader(icon: Icons.check_circle, title: 'Status'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Category and Status as cards
|
// Status card
|
||||||
Row(
|
_buildCategorySelect(
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildCategorySelect(
|
|
||||||
title: 'Kategori',
|
|
||||||
options: controller.categoryOptions,
|
|
||||||
selectedOption: controller.selectedCategory,
|
|
||||||
onChanged: controller.setCategory,
|
|
||||||
icon: Icons.inventory_2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: _buildCategorySelect(
|
|
||||||
title: 'Status',
|
title: 'Status',
|
||||||
options: controller.statusOptions,
|
options: controller.statusOptions,
|
||||||
selectedOption: controller.selectedStatus,
|
selectedOption: controller.selectedStatus,
|
||||||
onChanged: controller.setStatus,
|
onChanged: controller.setStatus,
|
||||||
icon: Icons.check_circle,
|
icon: Icons.check_circle,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Quantity Section
|
// Quantity Section
|
||||||
_buildSectionHeader(
|
_buildSectionHeader(
|
||||||
icon: Icons.format_list_numbered,
|
icon: Icons.format_list_numbered,
|
||||||
title: 'Kuantitas & Pengukuran',
|
title: 'Kuantitas',
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Quantity fields
|
// Quantity field
|
||||||
Row(
|
_buildTextField(
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
flex: 2,
|
|
||||||
child: _buildTextField(
|
|
||||||
label: 'Kuantitas',
|
label: 'Kuantitas',
|
||||||
hint: 'Jumlah aset',
|
hint: 'Jumlah aset',
|
||||||
controller: controller.quantityController,
|
controller: controller.quantityController,
|
||||||
@ -182,19 +140,6 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
|||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
prefixIcon: Icons.numbers,
|
prefixIcon: Icons.numbers,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
flex: 3,
|
|
||||||
child: _buildTextField(
|
|
||||||
label: 'Satuan Ukur',
|
|
||||||
hint: 'contoh: Unit, Buah',
|
|
||||||
controller: controller.unitOfMeasureController,
|
|
||||||
prefixIcon: Icons.straighten,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Rental Options Section
|
// Rental Options Section
|
||||||
@ -654,6 +599,114 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show image source options
|
||||||
|
void _showImageSourceOptions() {
|
||||||
|
Get.bottomSheet(
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(20),
|
||||||
|
topRight: Radius.circular(20),
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.2),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, -2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.only(bottom: 20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Pilih Sumber Gambar',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColorsPetugas.navyBlue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
_buildImageSourceOption(
|
||||||
|
icon: Icons.camera_alt,
|
||||||
|
label: 'Kamera',
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
controller.pickImageFromCamera();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildImageSourceOption(
|
||||||
|
icon: Icons.photo_library,
|
||||||
|
label: 'Galeri',
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
controller.pickImageFromGallery();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
child: const Text('Batal'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isScrollControlled: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImageSourceOption({
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 70,
|
||||||
|
height: 70,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColorsPetugas.babyBlueBright,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
size: 30,
|
||||||
|
color: AppColorsPetugas.blueGrotto,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColorsPetugas.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildImageUploader() {
|
Widget _buildImageUploader() {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@ -696,7 +749,7 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
|||||||
children: [
|
children: [
|
||||||
// Add button
|
// Add button
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => controller.addSampleImage(),
|
onTap: _showImageSourceOptions,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
@ -732,37 +785,76 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Image previews
|
// Image previews
|
||||||
...controller.selectedImages.asMap().entries.map((entry) {
|
...List<Widget>.generate(
|
||||||
final index = entry.key;
|
controller.selectedImages.length,
|
||||||
return Container(
|
(index) => Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColorsPetugas.babyBlueLight,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
boxShadow: [
|
border: Border.all(color: Colors.grey[300]!),
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.05),
|
|
||||||
blurRadius: 5,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
),
|
||||||
],
|
child: Obx(
|
||||||
|
() {
|
||||||
|
// Check if we have a network URL for this index
|
||||||
|
if (index < controller.networkImageUrls.length &&
|
||||||
|
controller.networkImageUrls[index].isNotEmpty) {
|
||||||
|
// Display network image
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Image.network(
|
||||||
|
controller.networkImageUrls[index],
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return const Center(
|
||||||
|
child: Icon(Icons.error_outline, color: Colors.red),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
child: Stack(
|
);
|
||||||
children: [
|
} else {
|
||||||
ClipRRect(
|
// Display local file
|
||||||
borderRadius: BorderRadius.circular(10),
|
return ClipRRect(
|
||||||
child: Container(
|
borderRadius: BorderRadius.circular(8),
|
||||||
width: 100,
|
child: FutureBuilder<File>(
|
||||||
height: 100,
|
future: File(controller.selectedImages[index].path).exists().then((exists) {
|
||||||
color: AppColorsPetugas.babyBlueLight,
|
if (exists) {
|
||||||
child: Center(
|
return File(controller.selectedImages[index].path);
|
||||||
child: Icon(
|
} else {
|
||||||
Icons.image,
|
return File(controller.selectedImages[index].path);
|
||||||
color: AppColorsPetugas.blueGrotto,
|
}
|
||||||
size: 40,
|
}),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData && snapshot.data != null) {
|
||||||
|
return Image.file(
|
||||||
|
snapshot.data!,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
child: const Icon(Icons.broken_image, color: Colors.grey),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@ -771,30 +863,29 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
|||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => controller.removeImage(index),
|
onTap: () => controller.removeImage(index),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(2),
|
||||||
decoration: BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.1),
|
color: Colors.black26,
|
||||||
blurRadius: 3,
|
blurRadius: 4,
|
||||||
offset: const Offset(0, 1),
|
offset: Offset(0, 1),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: const Icon(
|
||||||
Icons.close,
|
Icons.close,
|
||||||
color: AppColorsPetugas.error,
|
|
||||||
size: 16,
|
size: 16,
|
||||||
|
color: Colors.red,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
).toList(),
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -850,7 +941,9 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.save),
|
: const Icon(Icons.save),
|
||||||
label: Text(isSubmitting ? 'Menyimpan...' : 'Simpan Aset'),
|
label: Obx(() => Text(
|
||||||
|
isSubmitting ? 'Menyimpan...' : (controller.isEditing.value ? 'Simpan Perubahan' : 'Simpan Aset'),
|
||||||
|
)),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../../../theme/app_colors_petugas.dart';
|
import '../../../theme/app_colors_petugas.dart';
|
||||||
import '../controllers/petugas_tambah_paket_controller.dart';
|
import '../controllers/petugas_tambah_paket_controller.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||||
const PetugasTambahPaketView({Key? key}) : super(key: key);
|
const PetugasTambahPaketView({Key? key}) : super(key: key);
|
||||||
@ -12,9 +13,11 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text(
|
title: Obx(
|
||||||
'Tambah Paket',
|
() => Text(
|
||||||
style: TextStyle(fontWeight: FontWeight.w600),
|
controller.isEditing.value ? 'Edit Paket' : 'Tambah Paket',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: AppColorsPetugas.navyBlue,
|
backgroundColor: AppColorsPetugas.navyBlue,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
@ -24,7 +27,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [_buildHeaderSection(), _buildFormSection(context)],
|
children: [_buildFormSection(context)],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -32,64 +35,6 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeaderSection() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto],
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.2),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.category,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 28,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'Informasi Paket Baru',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'Isi data dengan lengkap untuk menambahkan paket',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.white.withOpacity(0.8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFormSection(BuildContext context) {
|
Widget _buildFormSection(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
@ -132,22 +77,22 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Category Section
|
// Category Section
|
||||||
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'),
|
_buildSectionHeader(icon: Icons.category, title: 'Status'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Category and Status as cards
|
// Category and Status as cards
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
// Expanded(
|
||||||
child: _buildCategorySelect(
|
// child: _buildCategorySelect(
|
||||||
title: 'Kategori',
|
// title: 'Kategori',
|
||||||
options: controller.categoryOptions,
|
// options: controller.categoryOptions,
|
||||||
selectedOption: controller.selectedCategory,
|
// selectedOption: controller.selectedCategory,
|
||||||
onChanged: controller.setCategory,
|
// onChanged: controller.setCategory,
|
||||||
icon: Icons.category,
|
// icon: Icons.category,
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
const SizedBox(width: 12),
|
// const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildCategorySelect(
|
child: _buildCategorySelect(
|
||||||
title: 'Status',
|
title: 'Status',
|
||||||
@ -161,24 +106,6 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Price Section
|
|
||||||
_buildSectionHeader(
|
|
||||||
icon: Icons.monetization_on,
|
|
||||||
title: 'Harga Paket',
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildTextField(
|
|
||||||
label: 'Harga Paket',
|
|
||||||
hint: 'Masukkan harga paket',
|
|
||||||
controller: controller.priceController,
|
|
||||||
isRequired: true,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
|
||||||
prefixText: 'Rp ',
|
|
||||||
prefixIcon: Icons.payments,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Package Items Section
|
// Package Items Section
|
||||||
_buildSectionHeader(
|
_buildSectionHeader(
|
||||||
icon: Icons.inventory_2,
|
icon: Icons.inventory_2,
|
||||||
@ -186,6 +113,40 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildPackageItems(),
|
_buildPackageItems(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildSectionHeader(
|
||||||
|
icon: Icons.schedule,
|
||||||
|
title: 'Opsi Waktu & Harga Sewa',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildTimeOptionsCards(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Obx(
|
||||||
|
() => Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (controller.timeOptions['Per Jam']!.value)
|
||||||
|
_buildPriceCard(
|
||||||
|
title: 'Harga Per Jam',
|
||||||
|
icon: Icons.timer,
|
||||||
|
priceController: controller.pricePerHourController,
|
||||||
|
maxController: controller.maxHourController,
|
||||||
|
maxLabel: 'Maksimal Jam',
|
||||||
|
),
|
||||||
|
if (controller.timeOptions['Per Jam']!.value &&
|
||||||
|
controller.timeOptions['Per Hari']!.value)
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (controller.timeOptions['Per Hari']!.value)
|
||||||
|
_buildPriceCard(
|
||||||
|
title: 'Harga Per Hari',
|
||||||
|
icon: Icons.calendar_today,
|
||||||
|
priceController: controller.pricePerDayController,
|
||||||
|
maxController: controller.maxDayController,
|
||||||
|
maxLabel: 'Maksimal Hari',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -310,7 +271,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Asset dropdown
|
// Asset dropdown
|
||||||
DropdownButtonFormField<int>(
|
DropdownButtonFormField<String>(
|
||||||
value: controller.selectedAsset.value,
|
value: controller.selectedAsset.value,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Pilih Aset',
|
labelText: 'Pilih Aset',
|
||||||
@ -319,8 +280,8 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
hint: const Text('Pilih Aset'),
|
hint: const Text('Pilih Aset'),
|
||||||
items:
|
items:
|
||||||
controller.availableAssets.map((asset) {
|
controller.availableAssets.map((asset) {
|
||||||
return DropdownMenuItem<int>(
|
return DropdownMenuItem<String>(
|
||||||
value: asset['id'] as int,
|
value: asset['id'].toString(),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${asset['nama']} (Stok: ${asset['stok']})',
|
'${asset['nama']} (Stok: ${asset['stok']})',
|
||||||
),
|
),
|
||||||
@ -422,7 +383,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Asset dropdown
|
// Asset dropdown
|
||||||
DropdownButtonFormField<int>(
|
DropdownButtonFormField<String>(
|
||||||
value: controller.selectedAsset.value,
|
value: controller.selectedAsset.value,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Pilih Aset',
|
labelText: 'Pilih Aset',
|
||||||
@ -431,8 +392,8 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
hint: const Text('Pilih Aset'),
|
hint: const Text('Pilih Aset'),
|
||||||
items:
|
items:
|
||||||
controller.availableAssets.map((asset) {
|
controller.availableAssets.map((asset) {
|
||||||
return DropdownMenuItem<int>(
|
return DropdownMenuItem<String>(
|
||||||
value: asset['id'] as int,
|
value: asset['id'].toString(),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${asset['nama']} (Stok: ${asset['stok']})',
|
'${asset['nama']} (Stok: ${asset['stok']})',
|
||||||
),
|
),
|
||||||
@ -757,7 +718,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
children: [
|
children: [
|
||||||
// Add button
|
// Add button
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => controller.addSampleImage(),
|
onTap: _showImageSourceOptions,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
@ -791,37 +752,58 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Image previews
|
// Image previews
|
||||||
...controller.selectedImages.asMap().entries.map((entry) {
|
...List<Widget>.generate(controller.selectedImages.length, (
|
||||||
final index = entry.key;
|
index,
|
||||||
return Container(
|
) {
|
||||||
|
final img = controller.selectedImages[index];
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColorsPetugas.babyBlueLight,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
boxShadow: [
|
border: Border.all(color: Colors.grey[300]!),
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.05),
|
|
||||||
blurRadius: 5,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
),
|
||||||
],
|
child: ClipRRect(
|
||||||
),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Stack(
|
child:
|
||||||
children: [
|
(img is String && img.startsWith('http'))
|
||||||
ClipRRect(
|
? Image.network(
|
||||||
borderRadius: BorderRadius.circular(10),
|
img,
|
||||||
child: Container(
|
fit: BoxFit.cover,
|
||||||
width: 100,
|
width: double.infinity,
|
||||||
height: 100,
|
height: double.infinity,
|
||||||
color: AppColorsPetugas.babyBlueLight,
|
errorBuilder:
|
||||||
child: Center(
|
(context, error, stackTrace) =>
|
||||||
|
const Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.image,
|
Icons.broken_image,
|
||||||
color: AppColorsPetugas.blueGrotto,
|
color: Colors.grey,
|
||||||
size: 40,
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: (img is String)
|
||||||
|
? Container(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
child: const Icon(
|
||||||
|
Icons.broken_image,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Image.file(
|
||||||
|
File(img.path),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
errorBuilder:
|
||||||
|
(context, error, stackTrace) =>
|
||||||
|
const Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.broken_image,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -829,35 +811,125 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
Positioned(
|
Positioned(
|
||||||
top: 4,
|
top: 4,
|
||||||
right: 4,
|
right: 4,
|
||||||
child: GestureDetector(
|
child: InkWell(
|
||||||
onTap: () => controller.removeImage(index),
|
onTap: () => controller.removeImage(index),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
boxShadow: [
|
),
|
||||||
BoxShadow(
|
child: const Icon(
|
||||||
color: Colors.black.withOpacity(0.1),
|
Icons.close,
|
||||||
blurRadius: 3,
|
size: 18,
|
||||||
offset: const Offset(0, 1),
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
child: Icon(
|
}),
|
||||||
Icons.close,
|
],
|
||||||
color: AppColorsPetugas.error,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}
|
||||||
|
|
||||||
|
void _showImageSourceOptions() {
|
||||||
|
Get.bottomSheet(
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(20),
|
||||||
|
topRight: Radius.circular(20),
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.2),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, -2),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.only(bottom: 20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Pilih Sumber Gambar',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColorsPetugas.navyBlue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
_buildImageSourceOption(
|
||||||
|
icon: Icons.camera_alt,
|
||||||
|
label: 'Kamera',
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
controller.pickImageFromCamera();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildImageSourceOption(
|
||||||
|
icon: Icons.photo_library,
|
||||||
|
label: 'Galeri',
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
controller.pickImageFromGallery();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImageSourceOption({
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColorsPetugas.babyBlueBright,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: AppColorsPetugas.blueGrotto, size: 28),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColorsPetugas.navyBlue,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -899,26 +971,37 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
final isSubmitting = controller.isSubmitting.value;
|
final isSubmitting = controller.isSubmitting.value;
|
||||||
return ElevatedButton.icon(
|
return ElevatedButton.icon(
|
||||||
onPressed:
|
onPressed:
|
||||||
isValid && !isSubmitting ? controller.savePaket : null,
|
controller.isFormChanged.value && !isSubmitting
|
||||||
|
? controller.savePaket
|
||||||
|
: null,
|
||||||
icon:
|
icon:
|
||||||
isSubmitting
|
isSubmitting
|
||||||
? SizedBox(
|
? const SizedBox(
|
||||||
height: 20,
|
width: 24,
|
||||||
width: 20,
|
height: 24,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.save),
|
: const Icon(Icons.save),
|
||||||
label: Text(isSubmitting ? 'Menyimpan...' : 'Simpan Paket'),
|
label: Text(
|
||||||
|
isSubmitting
|
||||||
|
? 'Menyimpan...'
|
||||||
|
: (controller.isEditing.value
|
||||||
|
? 'Simpan Paket'
|
||||||
|
: 'Tambah Paket'),
|
||||||
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
elevation: 0,
|
textStyle: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
disabledBackgroundColor: AppColorsPetugas.textLight,
|
disabledBackgroundColor: AppColorsPetugas.textLight,
|
||||||
),
|
),
|
||||||
@ -929,4 +1012,226 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTimeOptionsCards() {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children:
|
||||||
|
controller.timeOptions.entries.map((entry) {
|
||||||
|
final option = entry.key;
|
||||||
|
final isSelected = entry.value;
|
||||||
|
return Obx(
|
||||||
|
() => Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => controller.toggleTimeOption(option),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
isSelected.value
|
||||||
|
? AppColorsPetugas.blueGrotto.withOpacity(
|
||||||
|
0.1,
|
||||||
|
)
|
||||||
|
: Colors.grey.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
option == 'Per Jam'
|
||||||
|
? Icons.hourglass_bottom
|
||||||
|
: Icons.calendar_today,
|
||||||
|
color:
|
||||||
|
isSelected.value
|
||||||
|
? AppColorsPetugas.blueGrotto
|
||||||
|
: Colors.grey,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
option,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color:
|
||||||
|
isSelected.value
|
||||||
|
? AppColorsPetugas.navyBlue
|
||||||
|
: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
option == 'Per Jam'
|
||||||
|
? 'Sewa paket dengan basis perhitungan per jam'
|
||||||
|
: 'Sewa paket dengan basis perhitungan per hari',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Checkbox(
|
||||||
|
value: isSelected.value,
|
||||||
|
onChanged:
|
||||||
|
(_) => controller.toggleTimeOption(option),
|
||||||
|
activeColor: AppColorsPetugas.blueGrotto,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPriceCard({
|
||||||
|
required String title,
|
||||||
|
required IconData icon,
|
||||||
|
required TextEditingController priceController,
|
||||||
|
required TextEditingController maxController,
|
||||||
|
required String maxLabel,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: AppColorsPetugas.babyBlue.withOpacity(0.5)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.03),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 20, color: AppColorsPetugas.blueGrotto),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColorsPetugas.navyBlue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Harga Sewa',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppColorsPetugas.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
controller: priceController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Masukkan harga',
|
||||||
|
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
|
||||||
|
prefixText: 'Rp ',
|
||||||
|
filled: true,
|
||||||
|
fillColor: AppColorsPetugas.babyBlueBright,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
maxLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppColorsPetugas.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
controller: maxController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Opsional',
|
||||||
|
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
|
||||||
|
filled: true,
|
||||||
|
fillColor: AppColorsPetugas.babyBlueBright,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../../../theme/app_colors.dart';
|
import '../../../theme/app_colors.dart';
|
||||||
|
import '../../../theme/app_colors_petugas.dart';
|
||||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||||
|
|
||||||
class PetugasSideNavbar extends StatelessWidget {
|
class PetugasSideNavbar extends StatelessWidget {
|
||||||
@ -11,7 +12,7 @@ class PetugasSideNavbar extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Drawer(
|
return Drawer(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
@ -32,14 +33,17 @@ class PetugasSideNavbar extends StatelessWidget {
|
|||||||
Widget _buildHeader() {
|
Widget _buildHeader() {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
|
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
|
||||||
color: AppColors.primary,
|
color: AppColorsPetugas.navyBlue,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Obx(() {
|
||||||
|
final avatar = controller.avatarUrl.value;
|
||||||
|
if (avatar.isNotEmpty) {
|
||||||
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
@ -47,9 +51,28 @@ class PetugasSideNavbar extends StatelessWidget {
|
|||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 30,
|
radius: 30,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
child: Icon(Icons.person, color: AppColors.primary, size: 36),
|
backgroundImage: NetworkImage(avatar),
|
||||||
|
onBackgroundImageError: (error, stackTrace) {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
),
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 30,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
child: Icon(
|
||||||
|
Icons.person,
|
||||||
|
color: AppColors.primary,
|
||||||
|
size: 36,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@ -34,7 +34,7 @@ class SplashView extends GetView<SplashController> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: AssetImage('assets/images/pattern.png'),
|
image: AssetImage('assets/images/logo.png'), // Using logo.png which exists
|
||||||
repeat: ImageRepeat.repeat,
|
repeat: ImageRepeat.repeat,
|
||||||
scale: 4.0,
|
scale: 4.0,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -8,12 +8,6 @@ import '../../../data/providers/aset_provider.dart';
|
|||||||
class WargaSewaBinding extends Bindings {
|
class WargaSewaBinding extends Bindings {
|
||||||
@override
|
@override
|
||||||
void dependencies() {
|
void dependencies() {
|
||||||
// Ensure NavigationService is registered and set to Sewa tab
|
|
||||||
if (Get.isRegistered<NavigationService>()) {
|
|
||||||
final navService = Get.find<NavigationService>();
|
|
||||||
navService.setNavIndex(1); // Set to Sewa tab
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure AuthProvider is registered
|
// Ensure AuthProvider is registered
|
||||||
if (!Get.isRegistered<AuthProvider>()) {
|
if (!Get.isRegistered<AuthProvider>()) {
|
||||||
Get.put(AuthProvider(), permanent: true);
|
Get.put(AuthProvider(), permanent: true);
|
||||||
|
|||||||
@ -47,17 +47,21 @@ class PembayaranSewaController extends GetxController
|
|||||||
final isLoading = false.obs;
|
final isLoading = false.obs;
|
||||||
final currentStep = 0.obs;
|
final currentStep = 0.obs;
|
||||||
|
|
||||||
// Payment proof images - now a list to support multiple images (both File and WebImageFile)
|
// Payment proof images for tagihan awal
|
||||||
final RxList<dynamic> paymentProofImages = <dynamic>[].obs;
|
final RxList<dynamic> paymentProofImagesTagihanAwal = <dynamic>[].obs;
|
||||||
|
// Payment proof images for denda
|
||||||
|
final RxList<dynamic> paymentProofImagesDenda = <dynamic>[].obs;
|
||||||
|
|
||||||
// Track original images loaded from database
|
// Track original images loaded from database
|
||||||
final RxList<WebImageFile> originalImages = <WebImageFile>[].obs;
|
final RxList<WebImageFile> originalImages = <WebImageFile>[].obs;
|
||||||
|
|
||||||
// Track images marked for deletion
|
// Track images marked for deletion
|
||||||
final RxList<WebImageFile> imagesToDelete = <WebImageFile>[].obs;
|
final RxList<WebImageFile> imagesToDeleteTagihanAwal = <WebImageFile>[].obs;
|
||||||
|
final RxList<WebImageFile> imagesToDeleteDenda = <WebImageFile>[].obs;
|
||||||
|
|
||||||
// Flag to track if there are changes that need to be saved
|
// Flag to track if there are changes that need to be saved
|
||||||
final RxBool hasUnsavedChanges = false.obs;
|
final RxBool hasUnsavedChangesTagihanAwal = false.obs;
|
||||||
|
final RxBool hasUnsavedChangesDenda = false.obs;
|
||||||
|
|
||||||
// Get image widget for a specific image
|
// Get image widget for a specific image
|
||||||
Widget getImageWidget(dynamic imageFile) {
|
Widget getImageWidget(dynamic imageFile) {
|
||||||
@ -98,12 +102,7 @@ class PembayaranSewaController extends GetxController
|
|||||||
}
|
}
|
||||||
// For mobile with a File object
|
// For mobile with a File object
|
||||||
else if (imageFile is File) {
|
else if (imageFile is File) {
|
||||||
return Image.file(
|
return Image.file(imageFile, height: 120, width: 120, fit: BoxFit.cover);
|
||||||
imageFile,
|
|
||||||
height: 120,
|
|
||||||
width: 120,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Fallback for any other type
|
// Fallback for any other type
|
||||||
else {
|
else {
|
||||||
@ -118,18 +117,26 @@ class PembayaranSewaController extends GetxController
|
|||||||
|
|
||||||
// Remove an image from the list
|
// Remove an image from the list
|
||||||
void removeImage(dynamic image) {
|
void removeImage(dynamic image) {
|
||||||
// If this is an existing image (WebImageFile), add it to imagesToDelete
|
if (selectedPaymentType.value == 'denda') {
|
||||||
|
// Untuk denda
|
||||||
if (image is WebImageFile && image.id.isNotEmpty) {
|
if (image is WebImageFile && image.id.isNotEmpty) {
|
||||||
imagesToDelete.add(image);
|
imagesToDeleteDenda.add(image);
|
||||||
debugPrint('🗑️ Marked image for deletion: ${image.imageUrl} (ID: ${image.id})');
|
debugPrint(
|
||||||
|
'🗑️ Marked image for deletion (denda): \\${image.imageUrl} (ID: \\${image.id})',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
paymentProofImagesDenda.remove(image);
|
||||||
|
} else {
|
||||||
|
// Default/tagihan awal
|
||||||
|
if (image is WebImageFile && image.id.isNotEmpty) {
|
||||||
|
imagesToDeleteTagihanAwal.add(image);
|
||||||
|
debugPrint(
|
||||||
|
'🗑️ Marked image for deletion: \\${image.imageUrl} (ID: \\${image.id})',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
paymentProofImagesTagihanAwal.remove(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from the current list
|
|
||||||
paymentProofImages.remove(image);
|
|
||||||
|
|
||||||
// Check if we have any changes (additions or deletions)
|
|
||||||
_checkForChanges();
|
_checkForChanges();
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,14 +168,17 @@ class PembayaranSewaController extends GetxController
|
|||||||
panEnabled: true,
|
panEnabled: true,
|
||||||
minScale: 0.5,
|
minScale: 0.5,
|
||||||
maxScale: 4,
|
maxScale: 4,
|
||||||
child: kIsWeb
|
child:
|
||||||
|
kIsWeb
|
||||||
? Image.network(
|
? Image.network(
|
||||||
imageUrl,
|
imageUrl,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
height: Get.height,
|
height: Get.height,
|
||||||
width: Get.width,
|
width: Get.width,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
return const Center(child: Text('Error loading image'));
|
return const Center(
|
||||||
|
child: Text('Error loading image'),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: Image.file(
|
: Image.file(
|
||||||
@ -196,35 +206,33 @@ class PembayaranSewaController extends GetxController
|
|||||||
|
|
||||||
// Check if there are any changes to save (new images added or existing images removed)
|
// Check if there are any changes to save (new images added or existing images removed)
|
||||||
void _checkForChanges() {
|
void _checkForChanges() {
|
||||||
// We have changes if:
|
bool hasChangesTagihanAwal = false;
|
||||||
// 1. We have images marked for deletion
|
bool hasChangesDenda = false;
|
||||||
// 2. We have new images (files) added
|
if (imagesToDeleteTagihanAwal.isNotEmpty) {
|
||||||
// 3. The current list differs from the original list
|
hasChangesTagihanAwal = true;
|
||||||
|
|
||||||
bool hasChanges = false;
|
|
||||||
|
|
||||||
// Check if any images are marked for deletion
|
|
||||||
if (imagesToDelete.isNotEmpty) {
|
|
||||||
hasChanges = true;
|
|
||||||
}
|
}
|
||||||
|
if (imagesToDeleteDenda.isNotEmpty) {
|
||||||
// Check if any new images have been added
|
hasChangesDenda = true;
|
||||||
for (dynamic image in paymentProofImages) {
|
}
|
||||||
|
for (dynamic image in paymentProofImagesTagihanAwal) {
|
||||||
if (image is File) {
|
if (image is File) {
|
||||||
// This is a new image
|
hasChangesTagihanAwal = true;
|
||||||
hasChanges = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (dynamic image in paymentProofImagesDenda) {
|
||||||
// Check if the number of images has changed
|
if (image is File) {
|
||||||
if (paymentProofImages.length != originalImages.length) {
|
hasChangesDenda = true;
|
||||||
hasChanges = true;
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasUnsavedChangesTagihanAwal.value = hasChangesTagihanAwal;
|
||||||
|
hasUnsavedChangesDenda.value = hasChangesDenda;
|
||||||
|
debugPrint(
|
||||||
|
'💾 Has unsaved changes (tagihan awal): $hasChangesTagihanAwal, (denda): $hasChangesDenda',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasUnsavedChanges.value = hasChanges;
|
|
||||||
debugPrint('💾 Has unsaved changes: $hasChanges');
|
|
||||||
}
|
|
||||||
final isUploading = false.obs;
|
final isUploading = false.obs;
|
||||||
final uploadProgress = 0.0.obs;
|
final uploadProgress = 0.0.obs;
|
||||||
|
|
||||||
@ -260,8 +268,16 @@ class PembayaranSewaController extends GetxController
|
|||||||
'rental_period': rentalData['waktuSewa'] ?? '',
|
'rental_period': rentalData['waktuSewa'] ?? '',
|
||||||
'duration': rentalData['duration'] ?? '',
|
'duration': rentalData['duration'] ?? '',
|
||||||
'price_per_unit': 0, // This might not be available in rental data
|
'price_per_unit': 0, // This might not be available in rental data
|
||||||
'total_price': rentalData['totalPrice'] != null ?
|
'total_price':
|
||||||
int.tryParse(rentalData['totalPrice'].toString().replaceAll(RegExp(r'[^0-9]'), '')) ?? 0 : 0,
|
rentalData['totalPrice'] != null
|
||||||
|
? int.tryParse(
|
||||||
|
rentalData['totalPrice'].toString().replaceAll(
|
||||||
|
RegExp(r'[^0-9]'),
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
0
|
||||||
|
: 0,
|
||||||
'status': rentalData['status'] ?? 'MENUNGGU PEMBAYARAN',
|
'status': rentalData['status'] ?? 'MENUNGGU PEMBAYARAN',
|
||||||
'created_at': DateTime.now().toString(),
|
'created_at': DateTime.now().toString(),
|
||||||
'denda': 0, // Default value
|
'denda': 0, // Default value
|
||||||
@ -276,7 +292,7 @@ class PembayaranSewaController extends GetxController
|
|||||||
checkSewaAsetTableStructure();
|
checkSewaAsetTableStructure();
|
||||||
loadTagihanSewaDetails().then((_) {
|
loadTagihanSewaDetails().then((_) {
|
||||||
// Load existing payment proof images after tagihan_sewa details are loaded
|
// Load existing payment proof images after tagihan_sewa details are loaded
|
||||||
loadExistingPaymentProofImages();
|
loadExistingPaymentProofImages(jenisPembayaran: 'tagihan awal');
|
||||||
});
|
});
|
||||||
loadSewaAsetDetails();
|
loadSewaAsetDetails();
|
||||||
loadBankAccounts(); // Load bank accounts data
|
loadBankAccounts(); // Load bank accounts data
|
||||||
@ -286,7 +302,7 @@ class PembayaranSewaController extends GetxController
|
|||||||
loadOrderDetails();
|
loadOrderDetails();
|
||||||
loadTagihanSewaDetails().then((_) {
|
loadTagihanSewaDetails().then((_) {
|
||||||
// Load existing payment proof images after tagihan_sewa details are loaded
|
// Load existing payment proof images after tagihan_sewa details are loaded
|
||||||
loadExistingPaymentProofImages();
|
loadExistingPaymentProofImages(jenisPembayaran: 'tagihan awal');
|
||||||
});
|
});
|
||||||
loadSewaAsetDetails();
|
loadSewaAsetDetails();
|
||||||
loadBankAccounts(); // Load bank accounts data
|
loadBankAccounts(); // Load bank accounts data
|
||||||
@ -382,18 +398,19 @@ class PembayaranSewaController extends GetxController
|
|||||||
}
|
}
|
||||||
val?['quantity'] = data['kuantitas'] ?? 1;
|
val?['quantity'] = data['kuantitas'] ?? 1;
|
||||||
val?['denda'] =
|
val?['denda'] =
|
||||||
data['denda'] ??
|
data['denda'] ?? 0; // Use data from API or default to 0
|
||||||
0; // Use data from API or default to 0
|
val?['keterangan'] = data['keterangan'] ?? '';
|
||||||
val?['keterangan'] =
|
if (data['status'] != null &&
|
||||||
data['keterangan'] ??
|
data['status'].toString().isNotEmpty) {
|
||||||
''; // Use data from API or default to empty string
|
|
||||||
|
|
||||||
// Update status if it exists in the data
|
|
||||||
if (data['status'] != null && data['status'].toString().isNotEmpty) {
|
|
||||||
val?['status'] = data['status'];
|
val?['status'] = data['status'];
|
||||||
debugPrint('📊 Order status from sewa_aset: ${data['status']}');
|
debugPrint(
|
||||||
|
'📊 Order status from sewa_aset: \\${data['status']}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Tambahkan mapping updated_at
|
||||||
|
if (data['updated_at'] != null) {
|
||||||
|
val?['updated_at'] = data['updated_at'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format rental period
|
// Format rental period
|
||||||
if (data['waktu_mulai'] != null &&
|
if (data['waktu_mulai'] != null &&
|
||||||
data['waktu_selesai'] != null) {
|
data['waktu_selesai'] != null) {
|
||||||
@ -401,12 +418,12 @@ class PembayaranSewaController extends GetxController
|
|||||||
final startTime = DateTime.parse(data['waktu_mulai']);
|
final startTime = DateTime.parse(data['waktu_mulai']);
|
||||||
final endTime = DateTime.parse(data['waktu_selesai']);
|
final endTime = DateTime.parse(data['waktu_selesai']);
|
||||||
val?['rental_period'] =
|
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(
|
debugPrint(
|
||||||
'✅ Successfully formatted rental period: ${val?['rental_period']}',
|
'✅ Successfully formatted rental period: \\${val?['rental_period']}',
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error parsing date: $e');
|
debugPrint('❌ Error parsing date: \\${e}');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@ -547,6 +564,11 @@ class PembayaranSewaController extends GetxController
|
|||||||
// Select payment type (tagihan_awal or denda)
|
// Select payment type (tagihan_awal or denda)
|
||||||
void selectPaymentType(String type) {
|
void selectPaymentType(String type) {
|
||||||
selectedPaymentType.value = type;
|
selectedPaymentType.value = type;
|
||||||
|
if (type == 'tagihan_awal') {
|
||||||
|
loadExistingPaymentProofImages(jenisPembayaran: 'tagihan awal');
|
||||||
|
} else if (type == 'denda') {
|
||||||
|
loadExistingPaymentProofImages(jenisPembayaran: 'denda');
|
||||||
|
}
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -558,21 +580,20 @@ class PembayaranSewaController extends GetxController
|
|||||||
source: ImageSource.camera,
|
source: ImageSource.camera,
|
||||||
imageQuality: 80,
|
imageQuality: 80,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
// Add to the list of images instead of replacing
|
if (selectedPaymentType.value == 'denda') {
|
||||||
paymentProofImages.add(File(image.path));
|
paymentProofImagesDenda.add(File(image.path));
|
||||||
|
} else {
|
||||||
// Check for changes
|
paymentProofImagesTagihanAwal.add(File(image.path));
|
||||||
|
}
|
||||||
_checkForChanges();
|
_checkForChanges();
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error taking photo: $e');
|
debugPrint('❌ Error taking photo: $e');
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error',
|
'Error',
|
||||||
'Gagal mengambil foto: ${e.toString()}',
|
'Gagal mengambil foto: \\${e.toString()}',
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
colorText: Colors.white,
|
colorText: Colors.white,
|
||||||
@ -588,17 +609,20 @@ class PembayaranSewaController extends GetxController
|
|||||||
source: ImageSource.gallery,
|
source: ImageSource.gallery,
|
||||||
imageQuality: 80,
|
imageQuality: 80,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
// Add to the list of images instead of replacing
|
if (selectedPaymentType.value == 'denda') {
|
||||||
paymentProofImages.add(File(image.path));
|
paymentProofImagesDenda.add(File(image.path));
|
||||||
|
} else {
|
||||||
|
paymentProofImagesTagihanAwal.add(File(image.path));
|
||||||
|
}
|
||||||
|
_checkForChanges();
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error selecting photo from gallery: $e');
|
debugPrint('❌ Error selecting photo from gallery: $e');
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error',
|
'Error',
|
||||||
'Gagal memilih foto dari galeri: ${e.toString()}',
|
'Gagal memilih foto dari galeri: \\${e.toString()}',
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
colorText: Colors.white,
|
colorText: Colors.white,
|
||||||
@ -607,7 +631,19 @@ class PembayaranSewaController extends GetxController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upload payment proof to Supabase storage and save to foto_pembayaran table
|
// Upload payment proof to Supabase storage and save to foto_pembayaran table
|
||||||
Future<void> uploadPaymentProof() async {
|
Future<void> uploadPaymentProof({required String jenisPembayaran}) async {
|
||||||
|
final paymentProofImages =
|
||||||
|
jenisPembayaran == 'tagihan awal'
|
||||||
|
? paymentProofImagesTagihanAwal
|
||||||
|
: paymentProofImagesDenda;
|
||||||
|
final imagesToDelete =
|
||||||
|
jenisPembayaran == 'tagihan awal'
|
||||||
|
? imagesToDeleteTagihanAwal
|
||||||
|
: imagesToDeleteDenda;
|
||||||
|
final hasUnsavedChanges =
|
||||||
|
jenisPembayaran == 'tagihan awal'
|
||||||
|
? hasUnsavedChangesTagihanAwal
|
||||||
|
: hasUnsavedChangesDenda;
|
||||||
// If there are no images and none marked for deletion, show error
|
// If there are no images and none marked for deletion, show error
|
||||||
if (paymentProofImages.isEmpty && imagesToDelete.isEmpty) {
|
if (paymentProofImages.isEmpty && imagesToDelete.isEmpty) {
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
@ -644,7 +680,9 @@ class PembayaranSewaController extends GetxController
|
|||||||
|
|
||||||
// First, delete any images marked for deletion
|
// First, delete any images marked for deletion
|
||||||
if (imagesToDelete.isNotEmpty) {
|
if (imagesToDelete.isNotEmpty) {
|
||||||
debugPrint('🗑️ Deleting ${imagesToDelete.length} images from database and storage');
|
debugPrint(
|
||||||
|
'🗑️ Deleting ${imagesToDelete.length} images from database and storage',
|
||||||
|
);
|
||||||
|
|
||||||
for (WebImageFile image in imagesToDelete) {
|
for (WebImageFile image in imagesToDelete) {
|
||||||
// Delete the record from the foto_pembayaran table
|
// Delete the record from the foto_pembayaran table
|
||||||
@ -673,14 +711,16 @@ class PembayaranSewaController extends GetxController
|
|||||||
// The filename is the last part of the path after the last '/'
|
// The filename is the last part of the path after the last '/'
|
||||||
final String fileName = path.substring(path.lastIndexOf('/') + 1);
|
final String fileName = path.substring(path.lastIndexOf('/') + 1);
|
||||||
|
|
||||||
debugPrint('🗑️ Attempting to delete file from storage: $fileName');
|
debugPrint(
|
||||||
|
'🗑️ Attempting to delete file from storage: $fileName',
|
||||||
|
);
|
||||||
|
|
||||||
// Delete the file from storage
|
// Delete the file from storage
|
||||||
await client.storage
|
await client.storage.from('bukti.pembayaran').remove([fileName]);
|
||||||
.from('bukti.pembayaran')
|
|
||||||
.remove([fileName]);
|
|
||||||
|
|
||||||
debugPrint('🗑️ Successfully deleted file from storage: $fileName');
|
debugPrint(
|
||||||
|
'🗑️ Successfully deleted file from storage: $fileName',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('⚠️ Error deleting file from storage: $e');
|
debugPrint('⚠️ Error deleting file from storage: $e');
|
||||||
// Continue even if file deletion fails - we've at least deleted from the database
|
// Continue even if file deletion fails - we've at least deleted from the database
|
||||||
@ -693,7 +733,9 @@ class PembayaranSewaController extends GetxController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upload each new image to Supabase Storage and save to database
|
// Upload each new image to Supabase Storage and save to database
|
||||||
debugPrint('🔄 Uploading new payment proof images to Supabase storage...');
|
debugPrint(
|
||||||
|
'🔄 Uploading new payment proof images to Supabase storage...',
|
||||||
|
);
|
||||||
|
|
||||||
List<String> uploadedUrls = [];
|
List<String> uploadedUrls = [];
|
||||||
List<dynamic> newImagesToUpload = [];
|
List<dynamic> newImagesToUpload = [];
|
||||||
@ -710,7 +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 there are new images to upload
|
||||||
if (newImagesToUpload.isNotEmpty) {
|
if (newImagesToUpload.isNotEmpty) {
|
||||||
@ -721,13 +765,16 @@ class PembayaranSewaController extends GetxController
|
|||||||
// Upload each new image
|
// Upload each new image
|
||||||
for (int i = 0; i < newImagesToUpload.length; i++) {
|
for (int i = 0; i < newImagesToUpload.length; i++) {
|
||||||
final dynamic imageFile = newImagesToUpload[i];
|
final dynamic imageFile = newImagesToUpload[i];
|
||||||
final String fileName = '${DateTime.now().millisecondsSinceEpoch}_${orderId.value}_$i.jpg';
|
final String fileName =
|
||||||
|
'${DateTime.now().millisecondsSinceEpoch}_${orderId.value}_$i.jpg';
|
||||||
|
|
||||||
// Create a sub-progress tracker for this image
|
// Create a sub-progress tracker for this image
|
||||||
final subProgressNotifier = StreamController<double>();
|
final subProgressNotifier = StreamController<double>();
|
||||||
subProgressNotifier.stream.listen((subProgress) {
|
subProgressNotifier.stream.listen((subProgress) {
|
||||||
// Calculate overall progress
|
// Calculate overall progress
|
||||||
progressNotifier.add(currentProgress + (subProgress * progressIncrement));
|
progressNotifier.add(
|
||||||
|
currentProgress + (subProgress * progressIncrement),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Upload to Supabase Storage
|
// Upload to Supabase Storage
|
||||||
@ -754,15 +801,20 @@ class PembayaranSewaController extends GetxController
|
|||||||
|
|
||||||
// Save all new URLs to foto_pembayaran table
|
// Save all new URLs to foto_pembayaran table
|
||||||
for (String imageUrl in uploadedUrls) {
|
for (String imageUrl in uploadedUrls) {
|
||||||
await _saveToFotoPembayaranTable(imageUrl);
|
await _saveToFotoPembayaranTable(imageUrl, jenisPembayaran);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload the existing images to get fresh data with new IDs
|
// Reload the existing images to get fresh data with new IDs
|
||||||
await loadExistingPaymentProofImages();
|
await loadExistingPaymentProofImages(jenisPembayaran: jenisPembayaran);
|
||||||
|
|
||||||
// Update order status in orderDetails
|
// Update order status in orderDetails
|
||||||
orderDetails.update((val) {
|
orderDetails.update((val) {
|
||||||
|
if (jenisPembayaran == 'denda' &&
|
||||||
|
val?['status'] == 'PEMBAYARAN DENDA') {
|
||||||
|
val?['status'] = 'PERIKSA PEMBAYARAN DENDA';
|
||||||
|
} else {
|
||||||
val?['status'] = 'MEMERIKSA PEMBAYARAN';
|
val?['status'] = 'MEMERIKSA PEMBAYARAN';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also update the status in the sewa_aset table
|
// 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'];
|
final dynamic sewaAsetId = tagihanSewa.value['sewa_aset_id'];
|
||||||
|
|
||||||
if (sewaAsetId != null && sewaAsetId.toString().isNotEmpty) {
|
if (sewaAsetId != null && sewaAsetId.toString().isNotEmpty) {
|
||||||
debugPrint('🔄 Updating status in sewa_aset table for ID: $sewaAsetId');
|
debugPrint(
|
||||||
|
'🔄 Updating status in sewa_aset table for ID: $sewaAsetId',
|
||||||
|
);
|
||||||
|
|
||||||
// Update the status in the sewa_aset table
|
// Update the status in the sewa_aset table
|
||||||
final updateResult = await client
|
final updateResult = await client
|
||||||
.from('sewa_aset')
|
.from('sewa_aset')
|
||||||
.update({'status': 'PERIKSA PEMBAYARAN'})
|
.update({
|
||||||
|
'status':
|
||||||
|
(jenisPembayaran == 'denda' &&
|
||||||
|
orderDetails.value['status'] ==
|
||||||
|
'PERIKSA PEMBAYARAN DENDA')
|
||||||
|
? 'PERIKSA PEMBAYARAN DENDA'
|
||||||
|
: 'PERIKSA PEMBAYARAN',
|
||||||
|
})
|
||||||
.eq('id', sewaAsetId.toString());
|
.eq('id', sewaAsetId.toString());
|
||||||
|
|
||||||
debugPrint('✅ Status updated in sewa_aset table: $updateResult');
|
debugPrint('✅ Status updated in sewa_aset table: $updateResult');
|
||||||
} else {
|
} else {
|
||||||
debugPrint('⚠️ Could not update sewa_aset status: No valid sewa_aset_id found');
|
debugPrint(
|
||||||
|
'⚠️ Could not update sewa_aset status: No valid sewa_aset_id found',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Don't fail the entire operation if this update fails
|
// Don't fail the entire operation if this update fails
|
||||||
@ -878,21 +941,23 @@ class PembayaranSewaController extends GetxController
|
|||||||
case 'DITERIMA':
|
case 'DITERIMA':
|
||||||
currentStep.value = 2;
|
currentStep.value = 2;
|
||||||
break;
|
break;
|
||||||
case 'PENGEMBALIAN':
|
case 'AKTIF':
|
||||||
currentStep.value = 3;
|
currentStep.value = 3;
|
||||||
break;
|
break;
|
||||||
case 'PEMBAYARAN DENDA':
|
case 'PENGEMBALIAN':
|
||||||
currentStep.value = 4;
|
currentStep.value = 4;
|
||||||
break;
|
break;
|
||||||
case 'MEMERIKSA PEMBAYARAN DENDA':
|
case 'PEMBAYARAN DENDA':
|
||||||
currentStep.value = 5;
|
currentStep.value = 5;
|
||||||
break;
|
break;
|
||||||
case 'SELESAI':
|
case 'PERIKSA PEMBAYARAN DENDA':
|
||||||
currentStep.value = 6;
|
currentStep.value = 6;
|
||||||
break;
|
break;
|
||||||
|
case 'SELESAI':
|
||||||
|
currentStep.value = 7;
|
||||||
|
break;
|
||||||
case 'DIBATALKAN':
|
case 'DIBATALKAN':
|
||||||
// Special case for canceled orders
|
currentStep.value = 8;
|
||||||
currentStep.value = 0;
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
currentStep.value = 0;
|
currentStep.value = 0;
|
||||||
@ -950,7 +1015,9 @@ class PembayaranSewaController extends GetxController
|
|||||||
debugPrint('Available fields in sewa_aset table:');
|
debugPrint('Available fields in sewa_aset table:');
|
||||||
|
|
||||||
record.forEach((key, value) {
|
record.forEach((key, value) {
|
||||||
debugPrint(' $key: (${value != null ? value.runtimeType : 'null'})');
|
debugPrint(
|
||||||
|
' $key: (${value != null ? value.runtimeType : 'null'})',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Specifically check for time fields
|
// Specifically check for time fields
|
||||||
@ -987,12 +1054,16 @@ class PembayaranSewaController extends GetxController
|
|||||||
final data = await asetProvider.getBankAccounts();
|
final data = await asetProvider.getBankAccounts();
|
||||||
if (data.isNotEmpty) {
|
if (data.isNotEmpty) {
|
||||||
bankAccounts.assignAll(data);
|
bankAccounts.assignAll(data);
|
||||||
debugPrint('✅ Bank accounts loaded: ${bankAccounts.length} accounts found');
|
debugPrint(
|
||||||
|
'✅ Bank accounts loaded: ${bankAccounts.length} accounts found',
|
||||||
|
);
|
||||||
|
|
||||||
// Debug the bank accounts data
|
// Debug the bank accounts data
|
||||||
debugPrint('📋 BANK ACCOUNTS DETAILS:');
|
debugPrint('📋 BANK ACCOUNTS DETAILS:');
|
||||||
for (var account in bankAccounts) {
|
for (var account in bankAccounts) {
|
||||||
debugPrint(' Bank: ${account['nama_bank']}, Account: ${account['nama_akun']}, Number: ${account['no_rekening']}');
|
debugPrint(
|
||||||
|
' Bank: ${account['nama_bank']}, Account: ${account['nama_akun']}, Number: ${account['no_rekening']}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debugPrint('⚠️ No bank accounts found in akun_bank table');
|
debugPrint('⚠️ No bank accounts found in akun_bank table');
|
||||||
@ -1003,7 +1074,11 @@ class PembayaranSewaController extends GetxController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper method to upload image to Supabase storage
|
// Helper method to upload image to Supabase storage
|
||||||
Future<String?> _uploadToSupabaseStorage(dynamic imageFile, String fileName, StreamController<double> progressNotifier) async {
|
Future<String?> _uploadToSupabaseStorage(
|
||||||
|
dynamic imageFile,
|
||||||
|
String fileName,
|
||||||
|
StreamController<double> progressNotifier,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
debugPrint('🔄 Uploading image to Supabase storage: $fileName');
|
debugPrint('🔄 Uploading image to Supabase storage: $fileName');
|
||||||
|
|
||||||
@ -1031,7 +1106,9 @@ class PembayaranSewaController extends GetxController
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Get public URL
|
// Get public URL
|
||||||
final String publicUrl = client.storage.from('bukti.pembayaran').getPublicUrl(fileName);
|
final String publicUrl = client.storage
|
||||||
|
.from('bukti.pembayaran')
|
||||||
|
.getPublicUrl(fileName);
|
||||||
|
|
||||||
debugPrint('✅ Upload successful: $publicUrl');
|
debugPrint('✅ Upload successful: $publicUrl');
|
||||||
progressNotifier.add(1.0); // Upload complete
|
progressNotifier.add(1.0); // Upload complete
|
||||||
@ -1050,7 +1127,10 @@ class PembayaranSewaController extends GetxController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper method to save image URL to foto_pembayaran table
|
// Helper method to save image URL to foto_pembayaran table
|
||||||
Future<void> _saveToFotoPembayaranTable(String imageUrl) async {
|
Future<void> _saveToFotoPembayaranTable(
|
||||||
|
String imageUrl,
|
||||||
|
String jenisPembayaran,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
debugPrint('🔄 Saving image URL to foto_pembayaran table...');
|
debugPrint('🔄 Saving image URL to foto_pembayaran table...');
|
||||||
|
|
||||||
@ -1067,79 +1147,57 @@ class PembayaranSewaController extends GetxController
|
|||||||
final Map<String, dynamic> data = {
|
final Map<String, dynamic> data = {
|
||||||
'tagihan_sewa_id': tagihanSewaId,
|
'tagihan_sewa_id': tagihanSewaId,
|
||||||
'foto_pembayaran': imageUrl,
|
'foto_pembayaran': imageUrl,
|
||||||
|
'jenis_pembayaran': jenisPembayaran,
|
||||||
'created_at': DateTime.now().toIso8601String(),
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Insert data into the foto_pembayaran table
|
// Insert data into the foto_pembayaran table
|
||||||
final response = await client
|
final response =
|
||||||
.from('foto_pembayaran')
|
await client.from('foto_pembayaran').insert(data).select().single();
|
||||||
.insert(data)
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
debugPrint('✅ Image URL saved to foto_pembayaran table: ${response['id']}');
|
debugPrint(
|
||||||
|
'✅ Image URL saved to foto_pembayaran table: ${response['id']}',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error in _saveToFotoPembayaranTable: $e');
|
debugPrint('❌ Error in _saveToFotoPembayaranTable: $e');
|
||||||
throw Exception('Failed to save image URL to database: $e');
|
throw Exception('Failed to save image URL to database: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load existing payment proof images
|
// Load existing payment proof images for a specific jenis_pembayaran
|
||||||
Future<void> loadExistingPaymentProofImages() async {
|
Future<void> loadExistingPaymentProofImages({
|
||||||
|
required String jenisPembayaran,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
debugPrint('🔄 Loading existing payment proof images for tagihan_sewa_id: ${tagihanSewa.value['id']}');
|
debugPrint(
|
||||||
|
'🔄 Loading existing payment proof images for tagihan_sewa_id: \\${tagihanSewa.value['id']} dan jenis_pembayaran: $jenisPembayaran',
|
||||||
// Check if we have a valid tagihan_sewa_id
|
);
|
||||||
final dynamic tagihanSewaId = tagihanSewa.value['id'];
|
final dynamic tagihanSewaId = tagihanSewa.value['id'];
|
||||||
if (tagihanSewaId == null || tagihanSewaId.toString().isEmpty) {
|
if (tagihanSewaId == null || tagihanSewaId.toString().isEmpty) {
|
||||||
debugPrint('⚠️ No valid tagihan_sewa_id found, skipping image load');
|
debugPrint('⚠️ No valid tagihan_sewa_id found, skipping image load');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, make a test query to see the structure of the response
|
|
||||||
final testResponse = await client
|
|
||||||
.from('foto_pembayaran')
|
|
||||||
.select()
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// Log the test response structure
|
|
||||||
if (testResponse.isNotEmpty) {
|
|
||||||
debugPrint('💾 DEBUG: Test database response: ${testResponse[0]}');
|
|
||||||
testResponse[0].forEach((key, value) {
|
|
||||||
debugPrint('💾 DEBUG: Field $key = $value (${value?.runtimeType})');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now make the actual query for this tagihan_sewa_id
|
|
||||||
final List<dynamic> response = await client
|
final List<dynamic> response = await client
|
||||||
.from('foto_pembayaran')
|
.from('foto_pembayaran')
|
||||||
.select()
|
.select()
|
||||||
.eq('tagihan_sewa_id', tagihanSewaId)
|
.eq('tagihan_sewa_id', tagihanSewaId)
|
||||||
|
.eq('jenis_pembayaran', jenisPembayaran)
|
||||||
.order('created_at', ascending: false);
|
.order('created_at', ascending: false);
|
||||||
|
debugPrint(
|
||||||
debugPrint('🔄 Found ${response.length} existing payment proof images');
|
'🔄 Found \\${response.length} existing payment proof images for $jenisPembayaran',
|
||||||
|
);
|
||||||
// Clear existing tracking lists
|
final targetList =
|
||||||
paymentProofImages.clear();
|
jenisPembayaran == 'tagihan awal'
|
||||||
originalImages.clear();
|
? paymentProofImagesTagihanAwal
|
||||||
imagesToDelete.clear();
|
: paymentProofImagesDenda;
|
||||||
hasUnsavedChanges.value = false;
|
targetList.clear();
|
||||||
|
|
||||||
// Process each image in the response
|
|
||||||
for (final item in response) {
|
for (final item in response) {
|
||||||
// Extract the image URL
|
|
||||||
final String imageUrl = item['foto_pembayaran'];
|
final String imageUrl = item['foto_pembayaran'];
|
||||||
|
|
||||||
// Extract the ID - debug the item structure
|
|
||||||
debugPrint('💾 Image data: $item');
|
|
||||||
|
|
||||||
// Get the ID field - in Supabase, this is a UUID string
|
|
||||||
String imageId = '';
|
String imageId = '';
|
||||||
try {
|
try {
|
||||||
if (item.containsKey('id')) {
|
if (item.containsKey('id')) {
|
||||||
final dynamic rawId = item['id'];
|
final dynamic rawId = item['id'];
|
||||||
if (rawId != null) {
|
if (rawId != null) {
|
||||||
// Store ID as string since it's a UUID
|
|
||||||
imageId = rawId.toString();
|
imageId = rawId.toString();
|
||||||
}
|
}
|
||||||
debugPrint('🔄 Image ID: $imageId');
|
debugPrint('🔄 Image ID: $imageId');
|
||||||
@ -1147,21 +1205,12 @@ class PembayaranSewaController extends GetxController
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error getting image ID: $e');
|
debugPrint('❌ Error getting image ID: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the WebImageFile object
|
|
||||||
final webImageFile = WebImageFile(imageUrl);
|
final webImageFile = WebImageFile(imageUrl);
|
||||||
webImageFile.id = imageId;
|
webImageFile.id = imageId;
|
||||||
|
targetList.add(webImageFile);
|
||||||
// Add to tracking lists
|
|
||||||
paymentProofImages.add(webImageFile);
|
|
||||||
originalImages.add(webImageFile);
|
|
||||||
|
|
||||||
debugPrint('✅ Added image: $imageUrl with ID: $imageId');
|
debugPrint('✅ Added image: $imageUrl with ID: $imageId');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the UI
|
|
||||||
update();
|
update();
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error loading payment proof images: $e');
|
debugPrint('❌ Error loading payment proof images: $e');
|
||||||
}
|
}
|
||||||
@ -1174,7 +1223,9 @@ class PembayaranSewaController extends GetxController
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Reload all data
|
// Reload all data
|
||||||
await Future.delayed(const Duration(milliseconds: 500)); // Small delay for better UX
|
await Future.delayed(
|
||||||
|
const Duration(milliseconds: 500),
|
||||||
|
); // Small delay for better UX
|
||||||
loadOrderDetails();
|
loadOrderDetails();
|
||||||
loadTagihanSewaDetails();
|
loadTagihanSewaDetails();
|
||||||
loadSewaAsetDetails();
|
loadSewaAsetDetails();
|
||||||
|
|||||||
@ -88,6 +88,19 @@ class SewaAsetController extends GetxController
|
|||||||
void onReady() {
|
void onReady() {
|
||||||
super.onReady();
|
super.onReady();
|
||||||
debugPrint('🚀 SewaAsetController: onReady called');
|
debugPrint('🚀 SewaAsetController: onReady called');
|
||||||
|
// Set tab index from arguments (if any) after build
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
|
final args = Get.arguments;
|
||||||
|
if (args != null && args is Map && args['tab'] != null) {
|
||||||
|
int initialTab =
|
||||||
|
args['tab'] is int
|
||||||
|
? args['tab']
|
||||||
|
: int.tryParse(args['tab'].toString()) ?? 0;
|
||||||
|
if (tabController.length > initialTab) {
|
||||||
|
tabController.index = initialTab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -2,11 +2,14 @@ import 'package:get/get.dart';
|
|||||||
import '../../../data/providers/auth_provider.dart';
|
import '../../../data/providers/auth_provider.dart';
|
||||||
import '../../../routes/app_routes.dart';
|
import '../../../routes/app_routes.dart';
|
||||||
import '../../../services/navigation_service.dart';
|
import '../../../services/navigation_service.dart';
|
||||||
|
import '../../../data/providers/aset_provider.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class WargaDashboardController extends GetxController {
|
class WargaDashboardController extends GetxController {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
||||||
final NavigationService navigationService = Get.find<NavigationService>();
|
final NavigationService navigationService = Get.find<NavigationService>();
|
||||||
|
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
||||||
|
|
||||||
// User data
|
// User data
|
||||||
final userName = 'Pengguna Warga'.obs;
|
final userName = 'Pengguna Warga'.obs;
|
||||||
@ -28,6 +31,11 @@ class WargaDashboardController extends GetxController {
|
|||||||
// Active penalties
|
// Active penalties
|
||||||
final activePenalties = <Map<String, dynamic>>[].obs;
|
final activePenalties = <Map<String, dynamic>>[].obs;
|
||||||
|
|
||||||
|
// Summary counts
|
||||||
|
final diterimaCount = 0.obs;
|
||||||
|
final tagihanAktifCount = 0.obs;
|
||||||
|
final dendaAktifCount = 0.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
@ -36,6 +44,7 @@ class WargaDashboardController extends GetxController {
|
|||||||
navigationService.setNavIndex(0);
|
navigationService.setNavIndex(0);
|
||||||
|
|
||||||
// Load user data
|
// Load user data
|
||||||
|
fetchProfileFromWargaDesa();
|
||||||
_loadUserData();
|
_loadUserData();
|
||||||
|
|
||||||
// Load sample data
|
// Load sample data
|
||||||
@ -46,6 +55,12 @@ class WargaDashboardController extends GetxController {
|
|||||||
|
|
||||||
// Load unpaid rentals
|
// Load unpaid rentals
|
||||||
loadUnpaidRentals();
|
loadUnpaidRentals();
|
||||||
|
|
||||||
|
// Debug count sewa_aset by status
|
||||||
|
_debugCountSewaAset();
|
||||||
|
|
||||||
|
// Load sewa aktif
|
||||||
|
loadActiveRentals();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadUserData() async {
|
Future<void> _loadUserData() async {
|
||||||
@ -112,7 +127,7 @@ class WargaDashboardController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void refreshData() {
|
void refreshData() {
|
||||||
// Refresh data from repository
|
fetchProfileFromWargaDesa();
|
||||||
_loadSampleData();
|
_loadSampleData();
|
||||||
loadDummyData();
|
loadDummyData();
|
||||||
}
|
}
|
||||||
@ -129,12 +144,17 @@ class WargaDashboardController extends GetxController {
|
|||||||
// Already on Home tab
|
// Already on Home tab
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
// Navigate to Sewa page
|
// Navigate to Sewa page, tab Aktif
|
||||||
navigationService.toWargaSewa();
|
toWargaSewaTabAktif();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void toWargaSewaTabAktif() {
|
||||||
|
// Navigasi ke halaman warga sewa dan tab Aktif (index 3)
|
||||||
|
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 3});
|
||||||
|
}
|
||||||
|
|
||||||
void logout() async {
|
void logout() async {
|
||||||
await _authProvider.signOut();
|
await _authProvider.signOut();
|
||||||
navigationService.toLogin();
|
navigationService.toLogin();
|
||||||
@ -177,4 +197,137 @@ class WargaDashboardController extends GetxController {
|
|||||||
print('Error loading unpaid rentals: $e');
|
print('Error loading unpaid rentals: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _debugCountSewaAset() async {
|
||||||
|
diterimaCount.value = await _asetProvider.countSewaAsetByStatus([
|
||||||
|
'DITERIMA',
|
||||||
|
]);
|
||||||
|
tagihanAktifCount.value = await _asetProvider.countSewaAsetByStatus([
|
||||||
|
'MENUNGGU PEMBAYARAN',
|
||||||
|
'PERIKSA PEMBAYARAN',
|
||||||
|
]);
|
||||||
|
dendaAktifCount.value = await _asetProvider.countSewaAsetByStatus([
|
||||||
|
'PEMBAYARAN DENDA',
|
||||||
|
'PERIKSA PEMBAYARAN DENDA',
|
||||||
|
]);
|
||||||
|
print('[DEBUG] Jumlah sewa diterima: ${diterimaCount.value}');
|
||||||
|
print('[DEBUG] Jumlah tagihan aktif: ${tagihanAktifCount.value}');
|
||||||
|
print('[DEBUG] Jumlah denda aktif: ${dendaAktifCount.value}');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadActiveRentals() async {
|
||||||
|
try {
|
||||||
|
activeRentals.clear();
|
||||||
|
final sewaAsetList = await _authProvider.getSewaAsetByStatus(['AKTIF']);
|
||||||
|
for (var sewaAset in sewaAsetList) {
|
||||||
|
String assetName = 'Aset';
|
||||||
|
String? imageUrl;
|
||||||
|
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||||
|
if (sewaAset['aset_id'] != null) {
|
||||||
|
final asetData = await _asetProvider.getAsetById(sewaAset['aset_id']);
|
||||||
|
if (asetData != null) {
|
||||||
|
assetName = asetData.nama;
|
||||||
|
imageUrl = asetData.imageUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DateTime? waktuMulai;
|
||||||
|
DateTime? waktuSelesai;
|
||||||
|
String waktuSewa = '';
|
||||||
|
String tanggalSewa = '';
|
||||||
|
String jamMulai = '';
|
||||||
|
String jamSelesai = '';
|
||||||
|
String rentangWaktu = '';
|
||||||
|
if (sewaAset['waktu_mulai'] != null &&
|
||||||
|
sewaAset['waktu_selesai'] != null) {
|
||||||
|
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||||
|
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||||
|
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||||
|
final formatWaktu = DateFormat('HH:mm');
|
||||||
|
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||||
|
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||||
|
jamMulai = formatWaktu.format(waktuMulai);
|
||||||
|
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||||
|
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||||
|
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||||
|
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||||
|
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||||
|
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||||
|
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||||
|
} else {
|
||||||
|
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||||
|
}
|
||||||
|
waktuSewa =
|
||||||
|
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||||
|
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||||
|
}
|
||||||
|
String totalPrice = 'Rp 0';
|
||||||
|
if (sewaAset['total'] != null) {
|
||||||
|
final formatter = NumberFormat.currency(
|
||||||
|
locale: 'id',
|
||||||
|
symbol: 'Rp ',
|
||||||
|
decimalDigits: 0,
|
||||||
|
);
|
||||||
|
totalPrice = formatter.format(sewaAset['total']);
|
||||||
|
}
|
||||||
|
String duration = '-';
|
||||||
|
final tagihan = await _asetProvider.getTagihanSewa(sewaAset['id']);
|
||||||
|
if (tagihan != null) {
|
||||||
|
final durasiTagihan = tagihan['durasi'] ?? sewaAset['durasi'];
|
||||||
|
final satuanTagihan = tagihan['nama_satuan_waktu'] ?? namaSatuanWaktu;
|
||||||
|
duration = '${durasiTagihan ?? '-'} ${satuanTagihan ?? ''}';
|
||||||
|
} else {
|
||||||
|
duration = '${sewaAset['durasi'] ?? '-'} ${namaSatuanWaktu ?? ''}';
|
||||||
|
}
|
||||||
|
activeRentals.add({
|
||||||
|
'id': sewaAset['id'] ?? '',
|
||||||
|
'name': assetName,
|
||||||
|
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||||
|
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||||
|
'waktuSewa': waktuSewa,
|
||||||
|
'duration': duration,
|
||||||
|
'status': sewaAset['status'] ?? 'AKTIF',
|
||||||
|
'totalPrice': totalPrice,
|
||||||
|
'tanggalSewa': tanggalSewa,
|
||||||
|
'jamMulai': jamMulai,
|
||||||
|
'jamSelesai': jamSelesai,
|
||||||
|
'rentangWaktu': rentangWaktu,
|
||||||
|
'namaSatuanWaktu': namaSatuanWaktu,
|
||||||
|
'waktuMulai': sewaAset['waktu_mulai'],
|
||||||
|
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||||
|
'can_extend': sewaAset['can_extend'] == true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading active rentals: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void toSewaAsetTabPaket() {
|
||||||
|
// Navigasi ke halaman sewa_aset tab Paket (index 1)
|
||||||
|
Get.toNamed(Routes.SEWA_ASET, arguments: {'tab': 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchProfileFromWargaDesa() async {
|
||||||
|
try {
|
||||||
|
final user = _authProvider.currentUser;
|
||||||
|
if (user == null) return;
|
||||||
|
final userId = user.id;
|
||||||
|
final data =
|
||||||
|
await _authProvider.client
|
||||||
|
.from('warga_desa')
|
||||||
|
.select('nik, alamat, email, nama_lengkap, no_hp, avatar')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.maybeSingle();
|
||||||
|
if (data != null) {
|
||||||
|
userNik.value = data['nik']?.toString() ?? '';
|
||||||
|
userAddress.value = data['alamat']?.toString() ?? '';
|
||||||
|
userEmail.value = data['email']?.toString() ?? '';
|
||||||
|
userName.value = data['nama_lengkap']?.toString() ?? '';
|
||||||
|
userPhone.value = data['no_hp']?.toString() ?? '';
|
||||||
|
userAvatar.value = data['avatar']?.toString() ?? '';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching profile from warga_desa: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,8 @@ class WargaSewaController extends GetxController
|
|||||||
final acceptedRentals = <Map<String, dynamic>>[].obs;
|
final acceptedRentals = <Map<String, dynamic>>[].obs;
|
||||||
final completedRentals = <Map<String, dynamic>>[].obs;
|
final completedRentals = <Map<String, dynamic>>[].obs;
|
||||||
final cancelledRentals = <Map<String, dynamic>>[].obs;
|
final cancelledRentals = <Map<String, dynamic>>[].obs;
|
||||||
|
final returnedRentals = <Map<String, dynamic>>[].obs;
|
||||||
|
final activeRentals = <Map<String, dynamic>>[].obs;
|
||||||
|
|
||||||
// Loading states
|
// Loading states
|
||||||
final isLoading = false.obs;
|
final isLoading = false.obs;
|
||||||
@ -32,26 +34,26 @@ class WargaSewaController extends GetxController
|
|||||||
final isLoadingAccepted = false.obs;
|
final isLoadingAccepted = false.obs;
|
||||||
final isLoadingCompleted = false.obs;
|
final isLoadingCompleted = false.obs;
|
||||||
final isLoadingCancelled = false.obs;
|
final isLoadingCancelled = false.obs;
|
||||||
|
final isLoadingReturned = false.obs;
|
||||||
|
final isLoadingActive = false.obs;
|
||||||
|
|
||||||
|
bool _tabSetFromArgument = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
|
||||||
// Ensure tab index is set to Sewa (1)
|
// Initialize tab controller with 7 tabs
|
||||||
navigationService.setNavIndex(1);
|
tabController = TabController(length: 7, vsync: this);
|
||||||
|
|
||||||
// Initialize tab controller with 6 tabs
|
|
||||||
tabController = TabController(length: 6, vsync: this);
|
|
||||||
|
|
||||||
// Set initial tab and ensure tab view is updated
|
|
||||||
tabController.index = 0;
|
|
||||||
|
|
||||||
// Load real rental data for all tabs
|
// Load real rental data for all tabs
|
||||||
loadRentalsData();
|
loadRentalsData();
|
||||||
loadPendingRentals();
|
loadPendingRentals();
|
||||||
loadAcceptedRentals();
|
loadAcceptedRentals();
|
||||||
|
loadActiveRentals();
|
||||||
loadCompletedRentals();
|
loadCompletedRentals();
|
||||||
loadCancelledRentals();
|
loadCancelledRentals();
|
||||||
|
loadReturnedRentals();
|
||||||
|
|
||||||
// Listen to tab changes to update state if needed
|
// Listen to tab changes to update state if needed
|
||||||
tabController.addListener(() {
|
tabController.addListener(() {
|
||||||
@ -77,7 +79,9 @@ class WargaSewaController extends GetxController
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 3: // Aktif
|
case 3: // Aktif
|
||||||
// Add Aktif tab logic when needed
|
if (activeRentals.isEmpty && !isLoadingActive.value) {
|
||||||
|
loadActiveRentals();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 4: // Selesai
|
case 4: // Selesai
|
||||||
if (completedRentals.isEmpty && !isLoadingCompleted.value) {
|
if (completedRentals.isEmpty && !isLoadingCompleted.value) {
|
||||||
@ -89,6 +93,11 @@ class WargaSewaController extends GetxController
|
|||||||
loadCancelledRentals();
|
loadCancelledRentals();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 6: // Dikembalikan
|
||||||
|
if (returnedRentals.isEmpty && !isLoadingReturned.value) {
|
||||||
|
loadReturnedRentals();
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -96,9 +105,26 @@ class WargaSewaController extends GetxController
|
|||||||
@override
|
@override
|
||||||
void onReady() {
|
void onReady() {
|
||||||
super.onReady();
|
super.onReady();
|
||||||
// Ensure nav index is set to Sewa (1) when the controller is ready
|
// Jalankan update nav index dan tab index setelah build selesai
|
||||||
// This helps maintain correct state during hot reload
|
Future.delayed(Duration.zero, () {
|
||||||
navigationService.setNavIndex(1);
|
navigationService.setNavIndex(1);
|
||||||
|
|
||||||
|
final args = Get.arguments;
|
||||||
|
int initialTab = 0;
|
||||||
|
if (!_tabSetFromArgument &&
|
||||||
|
args != null &&
|
||||||
|
args is Map &&
|
||||||
|
args['tab'] != null) {
|
||||||
|
initialTab =
|
||||||
|
args['tab'] is int
|
||||||
|
? args['tab']
|
||||||
|
: int.tryParse(args['tab'].toString()) ?? 0;
|
||||||
|
if (tabController.length > initialTab) {
|
||||||
|
tabController.index = initialTab;
|
||||||
|
_tabSetFromArgument = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -118,7 +144,7 @@ class WargaSewaController extends GetxController
|
|||||||
// Get sewa_aset data with status "MENUNGGU PEMBAYARAN" or "PEMBAYARAN DENDA"
|
// Get sewa_aset data with status "MENUNGGU PEMBAYARAN" or "PEMBAYARAN DENDA"
|
||||||
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||||
'MENUNGGU PEMBAYARAN',
|
'MENUNGGU PEMBAYARAN',
|
||||||
'PEMBAYARAN DENDA'
|
'PEMBAYARAN DENDA',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
debugPrint('Fetched ${sewaAsetList.length} sewa_aset records');
|
debugPrint('Fetched ${sewaAsetList.length} sewa_aset records');
|
||||||
@ -147,7 +173,8 @@ class WargaSewaController extends GetxController
|
|||||||
String jamSelesai = '';
|
String jamSelesai = '';
|
||||||
String rentangWaktu = '';
|
String rentangWaktu = '';
|
||||||
|
|
||||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
if (sewaAset['waktu_mulai'] != null &&
|
||||||
|
sewaAset['waktu_selesai'] != null) {
|
||||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||||
|
|
||||||
@ -175,7 +202,8 @@ class WargaSewaController extends GetxController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Full time format for waktuSewa
|
// Full time format for waktuSewa
|
||||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
waktuSewa =
|
||||||
|
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,6 +236,7 @@ class WargaSewaController extends GetxController
|
|||||||
'namaSatuanWaktu': namaSatuanWaktu,
|
'namaSatuanWaktu': namaSatuanWaktu,
|
||||||
'waktuMulai': sewaAset['waktu_mulai'],
|
'waktuMulai': sewaAset['waktu_mulai'],
|
||||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||||
|
'updated_at': sewaAset['updated_at'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,12 +274,54 @@ class WargaSewaController extends GetxController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
void cancelRental(String id) {
|
void cancelRental(String id) async {
|
||||||
Get.snackbar(
|
final confirmed = await Get.dialog<bool>(
|
||||||
'Info',
|
AlertDialog(
|
||||||
'Pembatalan berhasil',
|
title: const Text('Konfirmasi Pembatalan'),
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
content: const Text('Apakah Anda yakin ingin membatalkan pesanan ini?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.back(result: false),
|
||||||
|
child: const Text('Tidak'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Get.back(result: true),
|
||||||
|
child: const Text('Ya, Batalkan'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
if (confirmed == true) {
|
||||||
|
try {
|
||||||
|
await asetProvider.client
|
||||||
|
.from('sewa_aset')
|
||||||
|
.update({'status': 'DIBATALKAN'})
|
||||||
|
.eq('id', id);
|
||||||
|
Get.snackbar(
|
||||||
|
'Berhasil',
|
||||||
|
'Pesanan berhasil dibatalkan',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
// Refresh data
|
||||||
|
loadRentalsData();
|
||||||
|
loadPendingRentals();
|
||||||
|
loadAcceptedRentals();
|
||||||
|
loadActiveRentals();
|
||||||
|
loadCompletedRentals();
|
||||||
|
loadCancelledRentals();
|
||||||
|
loadReturnedRentals();
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Gagal',
|
||||||
|
'Gagal membatalkan pesanan: $e',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to payment page with the selected rental data
|
// Navigate to payment page with the selected rental data
|
||||||
@ -260,10 +331,7 @@ class WargaSewaController extends GetxController
|
|||||||
// Navigate to payment page with rental data
|
// Navigate to payment page with rental data
|
||||||
Get.toNamed(
|
Get.toNamed(
|
||||||
Routes.PEMBAYARAN_SEWA,
|
Routes.PEMBAYARAN_SEWA,
|
||||||
arguments: {
|
arguments: {'orderId': rental['id'], 'rentalData': rental},
|
||||||
'orderId': rental['id'],
|
|
||||||
'rentalData': rental,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,7 +380,8 @@ class WargaSewaController extends GetxController
|
|||||||
String jamSelesai = '';
|
String jamSelesai = '';
|
||||||
String rentangWaktu = '';
|
String rentangWaktu = '';
|
||||||
|
|
||||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
if (sewaAset['waktu_mulai'] != null &&
|
||||||
|
sewaAset['waktu_selesai'] != null) {
|
||||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||||
|
|
||||||
@ -340,7 +409,8 @@ class WargaSewaController extends GetxController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Full time format for waktuSewa
|
// Full time format for waktuSewa
|
||||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
waktuSewa =
|
||||||
|
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,7 +445,9 @@ class WargaSewaController extends GetxController
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('Processed ${completedRentals.length} completed rental records');
|
debugPrint(
|
||||||
|
'Processed ${completedRentals.length} completed rental records',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error loading completed rentals data: $e');
|
debugPrint('Error loading completed rentals data: $e');
|
||||||
} finally {
|
} finally {
|
||||||
@ -392,7 +464,9 @@ class WargaSewaController extends GetxController
|
|||||||
cancelledRentals.clear();
|
cancelledRentals.clear();
|
||||||
|
|
||||||
// Get sewa_aset data with status "DIBATALKAN"
|
// Get sewa_aset data with status "DIBATALKAN"
|
||||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['DIBATALKAN']);
|
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||||
|
'DIBATALKAN',
|
||||||
|
]);
|
||||||
|
|
||||||
debugPrint('Fetched ${sewaAsetList.length} cancelled sewa_aset records');
|
debugPrint('Fetched ${sewaAsetList.length} cancelled sewa_aset records');
|
||||||
|
|
||||||
@ -420,7 +494,8 @@ class WargaSewaController extends GetxController
|
|||||||
String jamSelesai = '';
|
String jamSelesai = '';
|
||||||
String rentangWaktu = '';
|
String rentangWaktu = '';
|
||||||
|
|
||||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
if (sewaAset['waktu_mulai'] != null &&
|
||||||
|
sewaAset['waktu_selesai'] != null) {
|
||||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||||
|
|
||||||
@ -448,7 +523,8 @@ class WargaSewaController extends GetxController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Full time format for waktuSewa
|
// Full time format for waktuSewa
|
||||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
waktuSewa =
|
||||||
|
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -484,7 +560,9 @@ class WargaSewaController extends GetxController
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('Processed ${cancelledRentals.length} cancelled rental records');
|
debugPrint(
|
||||||
|
'Processed ${cancelledRentals.length} cancelled rental records',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error loading cancelled rentals data: $e');
|
debugPrint('Error loading cancelled rentals data: $e');
|
||||||
} finally {
|
} finally {
|
||||||
@ -500,8 +578,11 @@ class WargaSewaController extends GetxController
|
|||||||
// Clear existing data
|
// Clear existing data
|
||||||
pendingRentals.clear();
|
pendingRentals.clear();
|
||||||
|
|
||||||
// Get sewa_aset data with status "PERIKSA PEMBAYARAN"
|
// Get sewa_aset data with status 'PERIKSA PEMBAYARAN' dan 'PERIKSA PEMBAYARAN DENDA'
|
||||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['PERIKSA PEMBAYARAN']);
|
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||||
|
'PERIKSA PEMBAYARAN',
|
||||||
|
'PERIKSA PEMBAYARAN DENDA',
|
||||||
|
]);
|
||||||
|
|
||||||
debugPrint('Fetched ${sewaAsetList.length} pending sewa_aset records');
|
debugPrint('Fetched ${sewaAsetList.length} pending sewa_aset records');
|
||||||
|
|
||||||
@ -529,7 +610,8 @@ class WargaSewaController extends GetxController
|
|||||||
String jamSelesai = '';
|
String jamSelesai = '';
|
||||||
String rentangWaktu = '';
|
String rentangWaktu = '';
|
||||||
|
|
||||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
if (sewaAset['waktu_mulai'] != null &&
|
||||||
|
sewaAset['waktu_selesai'] != null) {
|
||||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||||
|
|
||||||
@ -557,7 +639,8 @@ class WargaSewaController extends GetxController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Full time format for waktuSewa
|
// Full time format for waktuSewa
|
||||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
waktuSewa =
|
||||||
|
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -637,7 +720,8 @@ class WargaSewaController extends GetxController
|
|||||||
String jamSelesai = '';
|
String jamSelesai = '';
|
||||||
String rentangWaktu = '';
|
String rentangWaktu = '';
|
||||||
|
|
||||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
if (sewaAset['waktu_mulai'] != null &&
|
||||||
|
sewaAset['waktu_selesai'] != null) {
|
||||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||||
|
|
||||||
@ -665,7 +749,8 @@ class WargaSewaController extends GetxController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Full time format for waktuSewa
|
// Full time format for waktuSewa
|
||||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
waktuSewa =
|
||||||
|
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -707,4 +792,166 @@ class WargaSewaController extends GetxController
|
|||||||
isLoadingAccepted.value = false;
|
isLoadingAccepted.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadReturnedRentals() async {
|
||||||
|
try {
|
||||||
|
isLoadingReturned.value = true;
|
||||||
|
returnedRentals.clear();
|
||||||
|
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||||
|
'DIKEMBALIKAN',
|
||||||
|
]);
|
||||||
|
for (var sewaAset in sewaAsetList) {
|
||||||
|
String assetName = 'Aset';
|
||||||
|
String? imageUrl;
|
||||||
|
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||||
|
if (sewaAset['aset_id'] != null) {
|
||||||
|
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||||
|
if (asetData != null) {
|
||||||
|
assetName = asetData.nama;
|
||||||
|
imageUrl = asetData.imageUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DateTime? waktuMulai;
|
||||||
|
DateTime? waktuSelesai;
|
||||||
|
String waktuSewa = '';
|
||||||
|
String tanggalSewa = '';
|
||||||
|
String jamMulai = '';
|
||||||
|
String jamSelesai = '';
|
||||||
|
String rentangWaktu = '';
|
||||||
|
if (sewaAset['waktu_mulai'] != null &&
|
||||||
|
sewaAset['waktu_selesai'] != null) {
|
||||||
|
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||||
|
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||||
|
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||||
|
final formatWaktu = DateFormat('HH:mm');
|
||||||
|
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||||
|
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||||
|
jamMulai = formatWaktu.format(waktuMulai);
|
||||||
|
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||||
|
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||||
|
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||||
|
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||||
|
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||||
|
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||||
|
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||||
|
} else {
|
||||||
|
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||||
|
}
|
||||||
|
waktuSewa =
|
||||||
|
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||||
|
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||||
|
}
|
||||||
|
String totalPrice = 'Rp 0';
|
||||||
|
if (sewaAset['total'] != null) {
|
||||||
|
final formatter = NumberFormat.currency(
|
||||||
|
locale: 'id',
|
||||||
|
symbol: 'Rp ',
|
||||||
|
decimalDigits: 0,
|
||||||
|
);
|
||||||
|
totalPrice = formatter.format(sewaAset['total']);
|
||||||
|
}
|
||||||
|
returnedRentals.add({
|
||||||
|
'id': sewaAset['id'] ?? '',
|
||||||
|
'name': assetName,
|
||||||
|
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||||
|
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||||
|
'waktuSewa': waktuSewa,
|
||||||
|
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||||
|
'status': sewaAset['status'] ?? 'DIKEMBALIKAN',
|
||||||
|
'totalPrice': totalPrice,
|
||||||
|
'tanggalSewa': tanggalSewa,
|
||||||
|
'jamMulai': jamMulai,
|
||||||
|
'jamSelesai': jamSelesai,
|
||||||
|
'rentangWaktu': rentangWaktu,
|
||||||
|
'namaSatuanWaktu': namaSatuanWaktu,
|
||||||
|
'waktuMulai': sewaAset['waktu_mulai'],
|
||||||
|
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error loading returned rentals data: $e');
|
||||||
|
} finally {
|
||||||
|
isLoadingReturned.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadActiveRentals() async {
|
||||||
|
try {
|
||||||
|
isLoadingActive.value = true;
|
||||||
|
activeRentals.clear();
|
||||||
|
final sewaAsetList = await authProvider.getSewaAsetByStatus(['AKTIF']);
|
||||||
|
for (var sewaAset in sewaAsetList) {
|
||||||
|
String assetName = 'Aset';
|
||||||
|
String? imageUrl;
|
||||||
|
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||||
|
if (sewaAset['aset_id'] != null) {
|
||||||
|
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||||
|
if (asetData != null) {
|
||||||
|
assetName = asetData.nama;
|
||||||
|
imageUrl = asetData.imageUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DateTime? waktuMulai;
|
||||||
|
DateTime? waktuSelesai;
|
||||||
|
String waktuSewa = '';
|
||||||
|
String tanggalSewa = '';
|
||||||
|
String jamMulai = '';
|
||||||
|
String jamSelesai = '';
|
||||||
|
String rentangWaktu = '';
|
||||||
|
if (sewaAset['waktu_mulai'] != null &&
|
||||||
|
sewaAset['waktu_selesai'] != null) {
|
||||||
|
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||||
|
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||||
|
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||||
|
final formatWaktu = DateFormat('HH:mm');
|
||||||
|
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||||
|
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||||
|
jamMulai = formatWaktu.format(waktuMulai);
|
||||||
|
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||||
|
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||||
|
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||||
|
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||||
|
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||||
|
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||||
|
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||||
|
} else {
|
||||||
|
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||||
|
}
|
||||||
|
waktuSewa =
|
||||||
|
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||||
|
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||||
|
}
|
||||||
|
String totalPrice = 'Rp 0';
|
||||||
|
if (sewaAset['total'] != null) {
|
||||||
|
final formatter = NumberFormat.currency(
|
||||||
|
locale: 'id',
|
||||||
|
symbol: 'Rp ',
|
||||||
|
decimalDigits: 0,
|
||||||
|
);
|
||||||
|
totalPrice = formatter.format(sewaAset['total']);
|
||||||
|
}
|
||||||
|
activeRentals.add({
|
||||||
|
'id': sewaAset['id'] ?? '',
|
||||||
|
'name': assetName,
|
||||||
|
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||||
|
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||||
|
'waktuSewa': waktuSewa,
|
||||||
|
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||||
|
'status': sewaAset['status'] ?? 'AKTIF',
|
||||||
|
'totalPrice': totalPrice,
|
||||||
|
'tanggalSewa': tanggalSewa,
|
||||||
|
'jamMulai': jamMulai,
|
||||||
|
'jamSelesai': jamSelesai,
|
||||||
|
'rentangWaktu': rentangWaktu,
|
||||||
|
'namaSatuanWaktu': namaSatuanWaktu,
|
||||||
|
'waktuMulai': sewaAset['waktu_mulai'],
|
||||||
|
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error loading active rentals data: $e');
|
||||||
|
} finally {
|
||||||
|
isLoadingActive.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import 'package:get/get.dart';
|
|||||||
import '../controllers/pembayaran_sewa_controller.dart';
|
import '../controllers/pembayaran_sewa_controller.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import '../../../theme/app_colors.dart';
|
import '../../../theme/app_colors.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
||||||
const PembayaranSewaView({super.key});
|
const PembayaranSewaView({super.key});
|
||||||
@ -81,6 +82,44 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if ((controller.orderDetails.value['status'] ?? '')
|
||||||
|
.toString()
|
||||||
|
.toUpperCase() ==
|
||||||
|
'MENUNGGU PEMBAYARAN' &&
|
||||||
|
controller.orderDetails.value['updated_at'] != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: Obx(() {
|
||||||
|
final status =
|
||||||
|
(controller.orderDetails.value['status'] ?? '')
|
||||||
|
.toString()
|
||||||
|
.toUpperCase();
|
||||||
|
final updatedAtStr =
|
||||||
|
controller.orderDetails.value['updated_at'];
|
||||||
|
print('DEBUG status: ' + status);
|
||||||
|
print(
|
||||||
|
'DEBUG updated_at (raw): ' +
|
||||||
|
(updatedAtStr?.toString() ?? 'NULL'),
|
||||||
|
);
|
||||||
|
if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) {
|
||||||
|
try {
|
||||||
|
final updatedAt = DateTime.parse(updatedAtStr);
|
||||||
|
print(
|
||||||
|
'DEBUG updated_at (parsed): ' +
|
||||||
|
updatedAt.toIso8601String(),
|
||||||
|
);
|
||||||
|
return CountdownTimerWidget(updatedAt: updatedAt);
|
||||||
|
} catch (e) {
|
||||||
|
print('ERROR parsing updated_at: ' + e.toString());
|
||||||
|
return Text(
|
||||||
|
'Format tanggal salah',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SizedBox.shrink();
|
||||||
|
}),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -174,30 +213,27 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
_buildPaymentTypeSelection(),
|
_buildPaymentTypeSelection(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
// Show payment method selection only after selecting a payment type
|
|
||||||
if (controller.selectedPaymentType.value.isNotEmpty) {
|
if (controller.selectedPaymentType.value.isNotEmpty) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildPaymentMethodSelection(),
|
_buildPaymentMethodSelection(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
if (controller.paymentMethod.value == 'transfer')
|
if (controller.paymentMethod.value == 'transfer') ...[
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildTransferInstructions(),
|
_buildTransferInstructions(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildPaymentProofUpload(),
|
if (controller.selectedPaymentType.value ==
|
||||||
],
|
'tagihan_awal')
|
||||||
)
|
_buildPaymentProofUploadTagihanAwal(),
|
||||||
else if (controller.paymentMethod.value == 'cash')
|
if (controller.selectedPaymentType.value == 'denda')
|
||||||
|
_buildPaymentProofUploadDenda(),
|
||||||
|
] else if (controller.paymentMethod.value == 'cash')
|
||||||
_buildCashInstructions()
|
_buildCashInstructions()
|
||||||
else
|
else
|
||||||
_buildSelectPaymentMethodPrompt(),
|
_buildSelectPaymentMethodPrompt(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Prompt to select payment type first
|
|
||||||
return _buildSelectPaymentTypePrompt();
|
return _buildSelectPaymentTypePrompt();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@ -272,18 +308,36 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
'Batas waktu pembayaran: ',
|
'Batas waktu pembayaran: ',
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||||
),
|
),
|
||||||
Obx(
|
Obx(() {
|
||||||
() => Text(
|
final status =
|
||||||
controller.orderDetails.value['status'] == 'DIBATALKAN'
|
(controller.orderDetails.value['status'] ?? '')
|
||||||
? 'Dibatalkan'
|
.toString()
|
||||||
: controller.remainingTime.value,
|
.toUpperCase();
|
||||||
style: TextStyle(
|
final updatedAtStr =
|
||||||
fontSize: 14,
|
controller.orderDetails.value['updated_at'];
|
||||||
fontWeight: FontWeight.bold,
|
print('DEBUG status: ' + status);
|
||||||
color: Colors.red[700],
|
print(
|
||||||
),
|
'DEBUG updated_at (raw): ' +
|
||||||
),
|
(updatedAtStr?.toString() ?? 'NULL'),
|
||||||
),
|
);
|
||||||
|
if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) {
|
||||||
|
try {
|
||||||
|
final updatedAt = DateTime.parse(updatedAtStr);
|
||||||
|
print(
|
||||||
|
'DEBUG updated_at (parsed): ' +
|
||||||
|
updatedAt.toIso8601String(),
|
||||||
|
);
|
||||||
|
return CountdownTimerWidget(updatedAt: updatedAt);
|
||||||
|
} catch (e) {
|
||||||
|
print('ERROR parsing updated_at: ' + e.toString());
|
||||||
|
return Text(
|
||||||
|
'Format tanggal salah',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SizedBox.shrink();
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -313,29 +367,41 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
'icon': Icons.check_circle,
|
'icon': Icons.check_circle,
|
||||||
'step': 2,
|
'step': 2,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'title': 'Aktif',
|
||||||
|
'description': 'Aset sewa sedang digunakan',
|
||||||
|
'icon': Icons.play_circle_fill,
|
||||||
|
'step': 3,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'title': 'Pengembalian',
|
'title': 'Pengembalian',
|
||||||
'description': 'Proses pengembalian aset sewa',
|
'description': 'Proses pengembalian aset sewa',
|
||||||
'icon': Icons.assignment_return,
|
'icon': Icons.assignment_return,
|
||||||
'step': 3,
|
'step': 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Pembayaran Denda',
|
'title': 'Pembayaran Denda',
|
||||||
'description': 'Pembayaran denda jika ada kerusakan atau keterlambatan',
|
'description': 'Pembayaran denda jika ada kerusakan atau keterlambatan',
|
||||||
'icon': Icons.money,
|
'icon': Icons.money,
|
||||||
'step': 4,
|
'step': 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Memeriksa Pembayaran Denda',
|
'title': 'Periksa Pembayaran Denda',
|
||||||
'description': 'Verifikasi pembayaran denda oleh petugas',
|
'description': 'Verifikasi pembayaran denda oleh petugas',
|
||||||
'icon': Icons.fact_check,
|
'icon': Icons.fact_check,
|
||||||
'step': 5,
|
'step': 6,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Selesai',
|
'title': 'Selesai',
|
||||||
'description': 'Pesanan sewa telah selesai',
|
'description': 'Pesanan sewa telah selesai',
|
||||||
'icon': Icons.task_alt,
|
'icon': Icons.task_alt,
|
||||||
'step': 6,
|
'step': 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Dibatalkan',
|
||||||
|
'description': 'Pesanan ini telah dibatalkan',
|
||||||
|
'icon': Icons.cancel,
|
||||||
|
'step': 8,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -360,37 +426,69 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final currentStep = controller.currentStep.value;
|
final currentStep = controller.currentStep.value;
|
||||||
|
final isCancelled = currentStep == 8;
|
||||||
|
// Filter steps: tampilkan step Dibatalkan hanya jika status DIBATALKAN
|
||||||
|
final visibleSteps =
|
||||||
|
isCancelled
|
||||||
|
? steps
|
||||||
|
: steps.where((s) => s['step'] != 8).toList();
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemCount: steps.length,
|
itemCount: visibleSteps.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final step = steps[index];
|
final step = visibleSteps[index];
|
||||||
final stepNumber = step['step'] as int;
|
final stepNumber = step['step'] as int;
|
||||||
final isActive = currentStep >= stepNumber;
|
final isActive =
|
||||||
final isCompleted = currentStep > stepNumber;
|
currentStep >= stepNumber &&
|
||||||
final isLast = index == steps.length - 1;
|
(!isCancelled || stepNumber == 8);
|
||||||
|
final isCompleted =
|
||||||
|
currentStep > stepNumber &&
|
||||||
|
(!isCancelled || stepNumber == 8);
|
||||||
|
final isLast = index == visibleSteps.length - 1;
|
||||||
|
|
||||||
// Determine the appropriate colors
|
// Custom color for dibatalkan
|
||||||
|
final bool isCancelledStep = stepNumber == 8;
|
||||||
final Color iconColor =
|
final Color iconColor =
|
||||||
isActive
|
isCancelledStep
|
||||||
|
? Colors.red
|
||||||
|
: isCancelled
|
||||||
|
? Colors.grey[400]!
|
||||||
|
: isActive
|
||||||
? (isCompleted
|
? (isCompleted
|
||||||
? AppColors.success
|
? AppColors.success
|
||||||
: AppColors.primary)
|
: AppColors.primary)
|
||||||
: Colors.grey[300]!;
|
: Colors.grey[300]!;
|
||||||
|
|
||||||
final Color lineColor =
|
final Color lineColor =
|
||||||
isCompleted ? AppColors.success : Colors.grey[300]!;
|
isCancelledStep
|
||||||
|
? Colors.red
|
||||||
|
: isCancelled
|
||||||
|
? Colors.grey[400]!
|
||||||
|
: isCompleted
|
||||||
|
? AppColors.success
|
||||||
|
: Colors.grey[300]!;
|
||||||
|
|
||||||
final Color bgColor =
|
final Color bgColor =
|
||||||
isActive
|
isCancelledStep
|
||||||
|
? Colors.red.withOpacity(0.1)
|
||||||
|
: isCancelled
|
||||||
|
? Colors.grey[100]!
|
||||||
|
: isActive
|
||||||
? (isCompleted
|
? (isCompleted
|
||||||
? AppColors.successLight
|
? AppColors.successLight
|
||||||
: AppColors.primarySoft)
|
: AppColors.primarySoft)
|
||||||
: Colors.grey[100]!;
|
: Colors.grey[100]!;
|
||||||
|
|
||||||
|
// Icon logic: silang untuk step lain jika dibatalkan
|
||||||
|
final IconData displayIcon =
|
||||||
|
isCancelled
|
||||||
|
? (isCancelledStep ? Icons.cancel : Icons.cancel)
|
||||||
|
: (isCompleted
|
||||||
|
? Icons.check
|
||||||
|
: step['icon'] as IconData);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
@ -403,9 +501,7 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
border: Border.all(color: iconColor, width: 2),
|
border: Border.all(color: iconColor, width: 2),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
isCompleted
|
displayIcon,
|
||||||
? Icons.check
|
|
||||||
: step['icon'] as IconData,
|
|
||||||
color: iconColor,
|
color: iconColor,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
@ -425,7 +521,9 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color:
|
color:
|
||||||
isActive
|
isCancelledStep
|
||||||
|
? Colors.red
|
||||||
|
: isActive
|
||||||
? AppColors.textPrimary
|
? AppColors.textPrimary
|
||||||
: AppColors.textSecondary,
|
: AppColors.textSecondary,
|
||||||
),
|
),
|
||||||
@ -434,7 +532,10 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
Text(
|
Text(
|
||||||
step['description'] as String,
|
step['description'] as String,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.textSecondary,
|
color:
|
||||||
|
isCancelledStep
|
||||||
|
? Colors.red
|
||||||
|
: AppColors.textSecondary,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -442,13 +543,18 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isCompleted)
|
if (isCancelled && isCancelledStep)
|
||||||
|
Icon(Icons.cancel, color: Colors.red, size: 18)
|
||||||
|
else if (!isCancelled && isCompleted)
|
||||||
Icon(
|
Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
color: AppColors.success,
|
color: AppColors.success,
|
||||||
size: 18,
|
size: 18,
|
||||||
)
|
)
|
||||||
else if (currentStep == stepNumber)
|
else if (isActive &&
|
||||||
|
!isCancelledStep &&
|
||||||
|
!isCompleted &&
|
||||||
|
!isCancelled)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8,
|
horizontal: 8,
|
||||||
@ -592,19 +698,20 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
'${controller.sewaAsetDetails.value['kuantitas'] ?? controller.orderDetails.value['quantity'] ?? 0} unit',
|
'${controller.sewaAsetDetails.value['kuantitas'] ?? controller.orderDetails.value['quantity'] ?? 0} unit',
|
||||||
),
|
),
|
||||||
// Waktu Sewa with sub-points for Waktu Mulai and Waktu Selesai
|
// Waktu Sewa with sub-points for Waktu Mulai and Waktu Selesai
|
||||||
_buildDetailItemWithSubpoints(
|
_buildDetailItemWithSubpoints('Waktu Sewa', [
|
||||||
'Waktu Sewa',
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
'label': 'Waktu Mulai',
|
'label': 'Waktu Mulai',
|
||||||
'value': _formatDateTime(controller.sewaAsetDetails.value['waktu_mulai']),
|
'value': _formatDateTime(
|
||||||
|
controller.sewaAsetDetails.value['waktu_mulai'],
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Waktu Selesai',
|
'label': 'Waktu Selesai',
|
||||||
'value': _formatDateTime(controller.sewaAsetDetails.value['waktu_selesai']),
|
'value': _formatDateTime(
|
||||||
},
|
controller.sewaAsetDetails.value['waktu_selesai'],
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
},
|
||||||
|
]),
|
||||||
_buildDetailItem(
|
_buildDetailItem(
|
||||||
'Durasi',
|
'Durasi',
|
||||||
controller.tagihanSewa.value['durasi'] != null
|
controller.tagihanSewa.value['durasi'] != null
|
||||||
@ -665,7 +772,8 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
// Get values from tagihan_sewa table
|
// Get values from tagihan_sewa table
|
||||||
final denda = controller.tagihanSewa.value['denda'];
|
final denda = controller.tagihanSewa.value['denda'];
|
||||||
final keterangan = controller.tagihanSewa.value['keterangan'];
|
final keterangan = controller.tagihanSewa.value['keterangan'];
|
||||||
final fotoKerusakan = controller.tagihanSewa.value['foto_kerusakan'];
|
final fotoKerusakan =
|
||||||
|
controller.tagihanSewa.value['foto_kerusakan'];
|
||||||
|
|
||||||
debugPrint('Tagihan Denda: $denda');
|
debugPrint('Tagihan Denda: $denda');
|
||||||
debugPrint('Tagihan Keterangan: $keterangan');
|
debugPrint('Tagihan Keterangan: $keterangan');
|
||||||
@ -748,18 +856,35 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
// Show fullscreen image when tapped
|
// Show fullscreen image when tapped
|
||||||
// Use the BuildContext from the current widget tree
|
// Use the BuildContext from the current widget tree
|
||||||
_showFullScreenImage(Get.context!, fotoKerusakan);
|
_showFullScreenImage(
|
||||||
|
Get.context!,
|
||||||
|
fotoKerusakan,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: 'damage-photo-${fotoKerusakan ?? 'default'}',
|
tag:
|
||||||
child: fotoKerusakan != null && fotoKerusakan.toString().isNotEmpty && fotoKerusakan.toString().startsWith('http')
|
'damage-photo-${fotoKerusakan ?? 'default'}',
|
||||||
|
child:
|
||||||
|
fotoKerusakan != null &&
|
||||||
|
fotoKerusakan
|
||||||
|
.toString()
|
||||||
|
.isNotEmpty &&
|
||||||
|
fotoKerusakan.toString().startsWith(
|
||||||
|
'http',
|
||||||
|
)
|
||||||
? Image.network(
|
? Image.network(
|
||||||
fotoKerusakan.toString(),
|
fotoKerusakan.toString(),
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 200,
|
height: 200,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (
|
||||||
debugPrint('Error loading image: $error');
|
context,
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
) {
|
||||||
|
debugPrint(
|
||||||
|
'Error loading image: $error',
|
||||||
|
);
|
||||||
return Image.asset(
|
return Image.asset(
|
||||||
'assets/images/gambar_pendukung.jpg',
|
'assets/images/gambar_pendukung.jpg',
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@ -880,7 +1005,8 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
|
|
||||||
// Show fullscreen image dialog
|
// Show fullscreen image dialog
|
||||||
void _showFullScreenImage(BuildContext context, dynamic imageUrl) {
|
void _showFullScreenImage(BuildContext context, dynamic imageUrl) {
|
||||||
final String imageSource = (imageUrl != null &&
|
final String imageSource =
|
||||||
|
(imageUrl != null &&
|
||||||
imageUrl.toString().isNotEmpty &&
|
imageUrl.toString().isNotEmpty &&
|
||||||
imageUrl.toString().startsWith('http'))
|
imageUrl.toString().startsWith('http'))
|
||||||
? imageUrl.toString()
|
? imageUrl.toString()
|
||||||
@ -906,12 +1032,15 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
height: MediaQuery.of(context).size.height,
|
height: MediaQuery.of(context).size.height,
|
||||||
color: Colors.black.withOpacity(0.8),
|
color: Colors.black.withOpacity(0.8),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: imageSource.isNotEmpty
|
child:
|
||||||
|
imageSource.isNotEmpty
|
||||||
? Image.network(
|
? Image.network(
|
||||||
imageSource,
|
imageSource,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
debugPrint('Error loading fullscreen image: $error');
|
debugPrint(
|
||||||
|
'Error loading fullscreen image: $error',
|
||||||
|
);
|
||||||
return Image.asset(
|
return Image.asset(
|
||||||
'assets/images/gambar_pendukung.jpg',
|
'assets/images/gambar_pendukung.jpg',
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
@ -1022,7 +1151,7 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
0;
|
0;
|
||||||
|
|
||||||
// Get denda value
|
// Get denda value
|
||||||
final denda = controller.sewaAsetDetails.value['denda'] ?? 0;
|
final denda = controller.tagihanSewa.value['denda'] ?? 0;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@ -1042,7 +1171,7 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
amount: 'Rp ${NumberFormat('#,###').format(denda)}',
|
amount: 'Rp ${NumberFormat('#,###').format(denda)}',
|
||||||
type: 'denda',
|
type: 'denda',
|
||||||
description: 'Pembayaran untuk denda yang diberikan',
|
description: 'Pembayaran untuk denda yang diberikan',
|
||||||
isDisabled: denda == 0,
|
isDisabled: denda == null || denda == 0,
|
||||||
isSelected: controller.selectedPaymentType.value == 'denda',
|
isSelected: controller.selectedPaymentType.value == 'denda',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1316,7 +1445,7 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
if (controller.bankAccounts.isEmpty) {
|
if (controller.isLoading.value) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: EdgeInsets.all(16.0),
|
||||||
@ -1324,9 +1453,21 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (controller.bankAccounts.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
'Tidak ada rekening bank yang tersedia.\nSilakan hubungi admin.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return Column(
|
return Column(
|
||||||
children: controller.bankAccounts.map((account) {
|
children:
|
||||||
|
controller.bankAccounts.map((account) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
_buildBankAccount(
|
_buildBankAccount(
|
||||||
@ -1365,13 +1506,6 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
title: 'Tunggu konfirmasi',
|
title: 'Tunggu konfirmasi',
|
||||||
description: 'Pembayaran Anda akan dikonfirmasi oleh petugas',
|
description: 'Pembayaran Anda akan dikonfirmasi oleh petugas',
|
||||||
),
|
),
|
||||||
_buildTransferStep(
|
|
||||||
icon: Icons.receipt_long,
|
|
||||||
title: 'Dapatkan struk pembayaran',
|
|
||||||
description:
|
|
||||||
'Setelah dikonfirmasi, akan dibuatkan struk pembayaran',
|
|
||||||
isLast: true,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1508,7 +1642,9 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
final scaffoldMessenger = ScaffoldMessenger.of(Get.context!);
|
final scaffoldMessenger = ScaffoldMessenger.of(Get.context!);
|
||||||
scaffoldMessenger.showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Nomor rekening $accountNumber disalin ke clipboard'),
|
content: Text(
|
||||||
|
'Nomor rekening $accountNumber disalin ke clipboard',
|
||||||
|
),
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
backgroundColor: Colors.green[700],
|
backgroundColor: Colors.green[700],
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
@ -1541,7 +1677,8 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Get the total price
|
// Get the total price
|
||||||
final totalPrice = controller.orderDetails.value['total_price'] ?? 0;
|
final totalPrice =
|
||||||
|
controller.orderDetails.value['total_price'] ?? 0;
|
||||||
// Format the total price as a number without 'Rp' prefix
|
// Format the total price as a number without 'Rp' prefix
|
||||||
final formattedPrice = totalPrice.toString();
|
final formattedPrice = totalPrice.toString();
|
||||||
|
|
||||||
@ -1552,7 +1689,9 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
final scaffoldMessenger = ScaffoldMessenger.of(Get.context!);
|
final scaffoldMessenger = ScaffoldMessenger.of(Get.context!);
|
||||||
scaffoldMessenger.showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Nominal Rp $formattedPrice disalin ke clipboard'),
|
content: Text(
|
||||||
|
'Nominal Rp $formattedPrice disalin ke clipboard',
|
||||||
|
),
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
backgroundColor: Colors.green[700],
|
backgroundColor: Colors.green[700],
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
@ -1570,11 +1709,7 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Icon(
|
Icon(Icons.copy, size: 14, color: Colors.deepPurple[300]),
|
||||||
Icons.copy,
|
|
||||||
size: 14,
|
|
||||||
color: Colors.deepPurple[300],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1635,8 +1770,8 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payment Proof Upload
|
// Payment Proof Upload for Tagihan Awal
|
||||||
Widget _buildPaymentProofUpload() {
|
Widget _buildPaymentProofUploadTagihanAwal() {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
@ -1661,48 +1796,55 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
spacing: 12,
|
spacing: 12,
|
||||||
runSpacing: 12,
|
runSpacing: 12,
|
||||||
children: [
|
children: [
|
||||||
// Display all existing images
|
|
||||||
...List.generate(
|
...List.generate(
|
||||||
controller.paymentProofImages.length,
|
controller.paymentProofImagesTagihanAwal.length,
|
||||||
(index) => _buildImageItem(index),
|
(index) => _buildImageItemTagihanAwal(index),
|
||||||
),
|
),
|
||||||
// Add photo button
|
_buildAddPhotoButtonTagihanAwal(),
|
||||||
_buildAddPhotoButton(),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Upload button
|
|
||||||
Obx(() {
|
Obx(() {
|
||||||
// Disable button if there are no changes or if upload is in progress
|
final bool isDisabled =
|
||||||
final bool isDisabled = controller.isUploading.value || !controller.hasUnsavedChanges.value;
|
controller.isUploading.value ||
|
||||||
|
!controller.hasUnsavedChangesTagihanAwal.value;
|
||||||
return ElevatedButton.icon(
|
return ElevatedButton.icon(
|
||||||
onPressed: isDisabled ? null : controller.uploadPaymentProof,
|
onPressed:
|
||||||
icon: controller.isUploading.value
|
isDisabled
|
||||||
|
? null
|
||||||
|
: () => controller.uploadPaymentProof(
|
||||||
|
jenisPembayaran: 'tagihan awal',
|
||||||
|
),
|
||||||
|
icon:
|
||||||
|
controller.isUploading.value
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Colors.white,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.save),
|
: const Icon(Icons.save),
|
||||||
label: Text(controller.isUploading.value
|
label: Text(
|
||||||
|
controller.isUploading.value
|
||||||
? 'Menyimpan...'
|
? 'Menyimpan...'
|
||||||
: (controller.hasUnsavedChanges.value ? 'Simpan' : 'Tidak Ada Perubahan')),
|
: (controller.hasUnsavedChangesTagihanAwal.value
|
||||||
|
? 'Simpan'
|
||||||
|
: 'Tidak Ada Perubahan'),
|
||||||
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.blue,
|
backgroundColor: Colors.blue,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
minimumSize: const Size(double.infinity, 48),
|
minimumSize: const Size(double.infinity, 48),
|
||||||
// Gray out button when disabled
|
|
||||||
disabledBackgroundColor: Colors.grey[300],
|
disabledBackgroundColor: Colors.grey[300],
|
||||||
disabledForegroundColor: Colors.grey[600],
|
disabledForegroundColor: Colors.grey[600],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
// Upload progress indicator
|
|
||||||
Obx(() {
|
Obx(() {
|
||||||
if (controller.isUploading.value) {
|
if (controller.isUploading.value) {
|
||||||
return Column(
|
return Column(
|
||||||
@ -1711,7 +1853,9 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
value: controller.uploadProgress.value,
|
value: controller.uploadProgress.value,
|
||||||
backgroundColor: Colors.grey[200],
|
backgroundColor: Colors.grey[200],
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue[700]!),
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Colors.blue[700]!,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
@ -1730,12 +1874,14 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build individual image item with remove button
|
Widget _buildImageItemTagihanAwal(int index) {
|
||||||
Widget _buildImageItem(int index) {
|
final image = controller.paymentProofImagesTagihanAwal[index];
|
||||||
final image = controller.paymentProofImages[index];
|
final status =
|
||||||
|
controller.orderDetails.value['status']?.toString().toUpperCase() ?? '';
|
||||||
|
final canDelete =
|
||||||
|
status == 'MENUNGGU PEMBAYARAN' || status == 'PERIKSA PEMBAYARAN';
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
// Make the container tappable to show full-screen image
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => controller.showFullScreenImage(image),
|
onTap: () => controller.showFullScreenImage(image),
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -1751,7 +1897,7 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Close/remove button remains the same
|
if (canDelete)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 4,
|
top: 4,
|
||||||
right: 4,
|
right: 4,
|
||||||
@ -1771,8 +1917,7 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build add photo button
|
Widget _buildAddPhotoButtonTagihanAwal() {
|
||||||
Widget _buildAddPhotoButton() {
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => _showImageSourceOptions(Get.context!),
|
onTap: () => _showImageSourceOptions(Get.context!),
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -1786,11 +1931,183 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(Icons.add_a_photo, size: 40, color: Colors.blue[700]),
|
||||||
Icons.add_a_photo,
|
const SizedBox(height: 8),
|
||||||
size: 40,
|
Text(
|
||||||
|
'Tambah Foto',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.blue[700],
|
color: Colors.blue[700],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payment Proof Upload for Denda
|
||||||
|
Widget _buildPaymentProofUploadDenda() {
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.photo_camera, size: 24),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Unggah Bukti Pembayaran',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Obx(() {
|
||||||
|
return Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 12,
|
||||||
|
children: [
|
||||||
|
...List.generate(
|
||||||
|
controller.paymentProofImagesDenda.length,
|
||||||
|
(index) => _buildImageItemDenda(index),
|
||||||
|
),
|
||||||
|
_buildAddPhotoButtonDenda(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Obx(() {
|
||||||
|
final bool isDisabled =
|
||||||
|
controller.isUploading.value ||
|
||||||
|
!controller.hasUnsavedChangesDenda.value;
|
||||||
|
return ElevatedButton.icon(
|
||||||
|
onPressed:
|
||||||
|
isDisabled
|
||||||
|
? null
|
||||||
|
: () => controller.uploadPaymentProof(
|
||||||
|
jenisPembayaran: 'denda',
|
||||||
|
),
|
||||||
|
icon:
|
||||||
|
controller.isUploading.value
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.save),
|
||||||
|
label: Text(
|
||||||
|
controller.isUploading.value
|
||||||
|
? 'Menyimpan...'
|
||||||
|
: (controller.hasUnsavedChangesDenda.value
|
||||||
|
? 'Simpan'
|
||||||
|
: 'Tidak Ada Perubahan'),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size(double.infinity, 48),
|
||||||
|
disabledBackgroundColor: Colors.grey[300],
|
||||||
|
disabledForegroundColor: Colors.grey[600],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
Obx(() {
|
||||||
|
if (controller.isUploading.value) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value: controller.uploadProgress.value,
|
||||||
|
backgroundColor: Colors.grey[200],
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Colors.blue[700]!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Mengunggah bukti pembayaran... ${(controller.uploadProgress.value * 100).toInt()}%',
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImageItemDenda(int index) {
|
||||||
|
final image = controller.paymentProofImagesDenda[index];
|
||||||
|
final status =
|
||||||
|
controller.orderDetails.value['status']?.toString().toUpperCase() ?? '';
|
||||||
|
final canDelete = status != 'SELESAI';
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => controller.showFullScreenImage(image),
|
||||||
|
child: Container(
|
||||||
|
width: 140,
|
||||||
|
height: 140,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.grey[300]!),
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: controller.getImageWidget(image),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (canDelete)
|
||||||
|
Positioned(
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => controller.removeImage(image),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAddPhotoButtonDenda() {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => _showImageSourceOptions(Get.context!),
|
||||||
|
child: Container(
|
||||||
|
width: 140,
|
||||||
|
height: 140,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue[50],
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.blue[200]!),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.add_a_photo, size: 40, color: Colors.blue[700]),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Tambah Foto',
|
'Tambah Foto',
|
||||||
@ -1871,13 +2188,6 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
description:
|
description:
|
||||||
'Total: Rp ${controller.orderDetails.value['total_price'] ?? 0}',
|
'Total: Rp ${controller.orderDetails.value['total_price'] ?? 0}',
|
||||||
),
|
),
|
||||||
_buildCashStep(
|
|
||||||
number: 4,
|
|
||||||
title: 'Dapatkan struk pembayaran',
|
|
||||||
description:
|
|
||||||
'Setelah dikonfirmasi, akan dibuatkan struk pembayaran',
|
|
||||||
isLast: true,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -2032,7 +2342,8 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
final denda = controller.tagihanSewa.value['denda'] ?? 0;
|
final denda = controller.tagihanSewa.value['denda'] ?? 0;
|
||||||
|
|
||||||
// Get total dibayarkan from tagihan_dibayar
|
// Get total dibayarkan from tagihan_dibayar
|
||||||
final dibayarkan = controller.tagihanSewa.value['tagihan_dibayar'] ?? 0;
|
final dibayarkan =
|
||||||
|
controller.tagihanSewa.value['tagihan_dibayar'] ?? 0;
|
||||||
|
|
||||||
debugPrint('Tagihan Awal: $tagihanAwal');
|
debugPrint('Tagihan Awal: $tagihanAwal');
|
||||||
debugPrint('Denda: $denda');
|
debugPrint('Denda: $denda');
|
||||||
@ -2080,7 +2391,10 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper method to build detail item with subpoints
|
// Helper method to build detail item with subpoints
|
||||||
Widget _buildDetailItemWithSubpoints(String label, List<Map<String, String>> subpoints) {
|
Widget _buildDetailItemWithSubpoints(
|
||||||
|
String label,
|
||||||
|
List<Map<String, String>> subpoints,
|
||||||
|
) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -2097,7 +2411,8 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// Subpoints with indentation
|
// Subpoints with indentation
|
||||||
...subpoints.map((subpoint) => Padding(
|
...subpoints.map(
|
||||||
|
(subpoint) => Padding(
|
||||||
padding: const EdgeInsets.only(left: 16, bottom: 6),
|
padding: const EdgeInsets.only(left: 16, bottom: 6),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@ -2107,25 +2422,21 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
flex: 2,
|
flex: 2,
|
||||||
child: Text(
|
child: Text(
|
||||||
subpoint['label'] ?? '',
|
subpoint['label'] ?? '',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 13, color: Colors.grey[600]),
|
||||||
fontSize: 13,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: Text(
|
child: Text(
|
||||||
subpoint['value'] ?? '-',
|
subpoint['value'] ?? '-',
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 13),
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -2152,3 +2463,85 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CountdownTimerWidget extends StatefulWidget {
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final VoidCallback? onTimeout;
|
||||||
|
const CountdownTimerWidget({
|
||||||
|
required this.updatedAt,
|
||||||
|
this.onTimeout,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
@override
|
||||||
|
State<CountdownTimerWidget> createState() => _CountdownTimerWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
|
||||||
|
late Duration remaining;
|
||||||
|
Timer? timer;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
print(
|
||||||
|
'DEBUG [CountdownTimerWidget] updatedAt: ' +
|
||||||
|
widget.updatedAt.toIso8601String(),
|
||||||
|
);
|
||||||
|
updateRemaining();
|
||||||
|
timer = Timer.periodic(
|
||||||
|
const Duration(seconds: 1),
|
||||||
|
(_) => updateRemaining(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateRemaining() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final end = widget.updatedAt.add(const Duration(hours: 1));
|
||||||
|
setState(() {
|
||||||
|
remaining = end.difference(now);
|
||||||
|
if (remaining.isNegative) {
|
||||||
|
remaining = Duration.zero;
|
||||||
|
timer?.cancel();
|
||||||
|
widget.onTimeout?.call();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (remaining.inSeconds <= 0) {
|
||||||
|
return Text('Waktu habis', style: TextStyle(color: Colors.red));
|
||||||
|
}
|
||||||
|
final h = remaining.inHours;
|
||||||
|
final m = remaining.inMinutes % 60;
|
||||||
|
final s = remaining.inSeconds % 60;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.withOpacity(0.08),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.red.withOpacity(0.3), width: 1),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.timer_outlined, size: 14, color: Colors.red),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Bayar dalam ${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -117,6 +117,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
dividerColor: Colors.transparent,
|
||||||
labelColor: Colors.white,
|
labelColor: Colors.white,
|
||||||
unselectedLabelColor: const Color(
|
unselectedLabelColor: const Color(
|
||||||
0xFF718093,
|
0xFF718093,
|
||||||
|
|||||||
@ -154,10 +154,10 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
'route': () => controller.navigateToRentals(),
|
'route': () => controller.navigateToRentals(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Bayar',
|
'title': 'Paket',
|
||||||
'icon': Icons.payment_outlined,
|
'icon': Icons.widgets_outlined,
|
||||||
'color': const Color(0xFF2196F3),
|
'color': const Color(0xFF2196F3),
|
||||||
'route': () => Get.toNamed(Routes.PEMBAYARAN_SEWA),
|
'route': () => controller.toSewaAsetTabPaket(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -218,32 +218,44 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Sewa Diterima
|
// Sewa Diterima
|
||||||
_buildActivityCard(
|
Obx(
|
||||||
|
() => _buildActivityCard(
|
||||||
title: 'Sewa Diterima',
|
title: 'Sewa Diterima',
|
||||||
value: controller.activeRentals.length.toString(),
|
value: controller.diterimaCount.value.toString(),
|
||||||
icon: Icons.check_circle_outline,
|
icon: Icons.check_circle_outline,
|
||||||
color: AppColors.success,
|
color: AppColors.success,
|
||||||
onTap: () => controller.navigateToRentals(),
|
onTap:
|
||||||
|
() =>
|
||||||
|
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 2}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// Tagihan Aktif
|
// Tagihan Aktif
|
||||||
_buildActivityCard(
|
Obx(
|
||||||
|
() => _buildActivityCard(
|
||||||
title: 'Tagihan Aktif',
|
title: 'Tagihan Aktif',
|
||||||
value: controller.activeBills.length.toString(),
|
value: controller.tagihanAktifCount.value.toString(),
|
||||||
icon: Icons.receipt_long_outlined,
|
icon: Icons.receipt_long_outlined,
|
||||||
color: AppColors.warning,
|
color: AppColors.warning,
|
||||||
onTap: () => Get.toNamed(Routes.PEMBAYARAN_SEWA),
|
onTap:
|
||||||
|
() =>
|
||||||
|
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 0}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// Denda Aktif
|
// Denda Aktif
|
||||||
_buildActivityCard(
|
Obx(
|
||||||
|
() => _buildActivityCard(
|
||||||
title: 'Denda Aktif',
|
title: 'Denda Aktif',
|
||||||
value: controller.activePenalties.length.toString(),
|
value: controller.dendaAktifCount.value.toString(),
|
||||||
icon: Icons.warning_amber_outlined,
|
icon: Icons.warning_amber_outlined,
|
||||||
color: AppColors.error,
|
color: AppColors.error,
|
||||||
onTap: () => Get.toNamed(Routes.PEMBAYARAN_SEWA),
|
onTap:
|
||||||
|
() =>
|
||||||
|
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 0}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -357,7 +369,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Sewa Diterima',
|
'Sewa Aktif',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -498,31 +510,34 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Asset icon
|
// Asset icon/gambar
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(12),
|
width: 48,
|
||||||
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
colors: [
|
|
||||||
AppColors.primary.withOpacity(0.7),
|
|
||||||
AppColors.primary,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
boxShadow: [
|
color: AppColors.primary.withOpacity(0.08),
|
||||||
BoxShadow(
|
|
||||||
color: AppColors.primary.withOpacity(0.3),
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: const Offset(0, 3),
|
|
||||||
),
|
),
|
||||||
],
|
child: ClipRRect(
|
||||||
),
|
borderRadius: BorderRadius.circular(14),
|
||||||
child: const Icon(
|
child:
|
||||||
|
rental['imageUrl'] != null &&
|
||||||
|
rental['imageUrl'].toString().isNotEmpty
|
||||||
|
? Image.network(
|
||||||
|
rental['imageUrl'],
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder:
|
||||||
|
(context, error, stackTrace) => Icon(
|
||||||
Icons.local_shipping,
|
Icons.local_shipping,
|
||||||
color: Colors.white,
|
color: AppColors.primary,
|
||||||
size: 24,
|
size: 28,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
Icons.local_shipping,
|
||||||
|
color: AppColors.primary,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
@ -533,7 +548,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
rental['name'],
|
rental['name'] ?? '-',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -542,7 +557,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
rental['time'],
|
rental['waktuSewa'] ?? '',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.textSecondary,
|
||||||
@ -567,7 +582,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
rental['price'],
|
rental['totalPrice'] ?? 'Rp 0',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.primary,
|
color: AppColors.primary,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -591,14 +606,14 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
child: _buildInfoItem(
|
child: _buildInfoItem(
|
||||||
icon: Icons.timer_outlined,
|
icon: Icons.timer_outlined,
|
||||||
title: 'Durasi',
|
title: 'Durasi',
|
||||||
value: rental['duration'],
|
value: rental['duration'] ?? '-',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildInfoItem(
|
child: _buildInfoItem(
|
||||||
icon: Icons.calendar_today_outlined,
|
icon: Icons.calendar_today_outlined,
|
||||||
title: 'Status',
|
title: 'Status',
|
||||||
value: 'Diterima',
|
value: rental['status'] ?? '-',
|
||||||
valueColor: AppColors.success,
|
valueColor: AppColors.success,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -608,7 +623,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons
|
||||||
if (rental['can_extend'])
|
if ((rental['can_extend'] ?? false) == true)
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: () => controller.extendRental(rental['id']),
|
onPressed: () => controller.extendRental(rental['id']),
|
||||||
icon: const Icon(Icons.update, size: 18),
|
icon: const Icon(Icons.update, size: 18),
|
||||||
|
|||||||
@ -3,12 +3,16 @@ import 'package:get/get.dart';
|
|||||||
import '../controllers/warga_dashboard_controller.dart';
|
import '../controllers/warga_dashboard_controller.dart';
|
||||||
import '../views/warga_layout.dart';
|
import '../views/warga_layout.dart';
|
||||||
import '../../../theme/app_colors.dart';
|
import '../../../theme/app_colors.dart';
|
||||||
|
import '../../../widgets/app_drawer.dart';
|
||||||
|
import '../../../services/navigation_service.dart';
|
||||||
|
|
||||||
class WargaProfileView extends GetView<WargaDashboardController> {
|
class WargaProfileView extends GetView<WargaDashboardController> {
|
||||||
const WargaProfileView({super.key});
|
const WargaProfileView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final navigationService = Get.find<NavigationService>();
|
||||||
|
navigationService.setNavIndex(2);
|
||||||
return WargaLayout(
|
return WargaLayout(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Profil Saya'),
|
title: const Text('Profil Saya'),
|
||||||
@ -29,6 +33,14 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
drawer: AppDrawer(
|
||||||
|
onNavItemTapped: (index) {
|
||||||
|
// Handle navigation if needed
|
||||||
|
},
|
||||||
|
onLogout: () {
|
||||||
|
controller.logout();
|
||||||
|
},
|
||||||
|
),
|
||||||
backgroundColor: Colors.grey.shade100,
|
backgroundColor: Colors.grey.shade100,
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
color: AppColors.primary,
|
color: AppColors.primary,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import '../views/warga_layout.dart';
|
|||||||
import '../../../services/navigation_service.dart';
|
import '../../../services/navigation_service.dart';
|
||||||
import '../../../widgets/app_drawer.dart';
|
import '../../../widgets/app_drawer.dart';
|
||||||
import '../../../theme/app_colors.dart';
|
import '../../../theme/app_colors.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
class WargaSewaView extends GetView<WargaSewaController> {
|
class WargaSewaView extends GetView<WargaSewaController> {
|
||||||
const WargaSewaView({super.key});
|
const WargaSewaView({super.key});
|
||||||
@ -50,6 +51,7 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
_buildPendingTab(),
|
_buildPendingTab(),
|
||||||
_buildDiterimaTab(),
|
_buildDiterimaTab(),
|
||||||
_buildAktifTab(),
|
_buildAktifTab(),
|
||||||
|
_buildDikembalikanTab(),
|
||||||
_buildSelesaiTab(),
|
_buildSelesaiTab(),
|
||||||
_buildDibatalkanTab(),
|
_buildDibatalkanTab(),
|
||||||
],
|
],
|
||||||
@ -119,6 +121,7 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
_buildTab(text: 'Pending', icon: Icons.pending_outlined),
|
_buildTab(text: 'Pending', icon: Icons.pending_outlined),
|
||||||
_buildTab(text: 'Diterima', icon: Icons.check_circle_outline),
|
_buildTab(text: 'Diterima', icon: Icons.check_circle_outline),
|
||||||
_buildTab(text: 'Aktif', icon: Icons.play_circle_outline),
|
_buildTab(text: 'Aktif', icon: Icons.play_circle_outline),
|
||||||
|
_buildTab(text: 'Dikembalikan', icon: Icons.assignment_return),
|
||||||
_buildTab(text: 'Selesai', icon: Icons.task_alt_outlined),
|
_buildTab(text: 'Selesai', icon: Icons.task_alt_outlined),
|
||||||
_buildTab(text: 'Dibatalkan', icon: Icons.cancel_outlined),
|
_buildTab(text: 'Dibatalkan', icon: Icons.cancel_outlined),
|
||||||
],
|
],
|
||||||
@ -147,9 +150,7 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
return Obx(() {
|
return Obx(() {
|
||||||
// Show loading indicator while fetching data
|
// Show loading indicator while fetching data
|
||||||
if (controller.isLoadingPending.value) {
|
if (controller.isLoadingPending.value) {
|
||||||
return const Center(
|
return const Center(child: CircularProgressIndicator());
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there is any data to display
|
// Check if there is any data to display
|
||||||
@ -158,11 +159,14 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: controller.pendingRentals
|
children:
|
||||||
.map((rental) => Padding(
|
controller.pendingRentals
|
||||||
|
.map(
|
||||||
|
(rental) => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
child: _buildUnpaidRentalCard(rental),
|
child: _buildUnpaidRentalCard(rental),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -182,46 +186,189 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
|
|
||||||
Widget _buildAktifTab() {
|
Widget _buildAktifTab() {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
// Show loading indicator while fetching data
|
if (controller.isLoadingActive.value) {
|
||||||
if (controller.isLoading.value) {
|
return const Center(child: CircularProgressIndicator());
|
||||||
return const Center(
|
}
|
||||||
child: CircularProgressIndicator(),
|
if (controller.activeRentals.isEmpty) {
|
||||||
|
return _buildTabContent(
|
||||||
|
icon: Icons.play_circle_outline,
|
||||||
|
title: 'Tidak ada sewa aktif',
|
||||||
|
subtitle: 'Sewa yang sedang berlangsung akan muncul di sini',
|
||||||
|
buttonText: 'Sewa Sekarang',
|
||||||
|
onButtonPressed: () => controller.navigateToRentals(),
|
||||||
|
color: Colors.blue,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return SingleChildScrollView(
|
||||||
// Placeholder content for the Aktif tab
|
physics: const BouncingScrollPhysics(),
|
||||||
return const Center(
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
children:
|
||||||
|
controller.activeRentals
|
||||||
|
.map(
|
||||||
|
(rental) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
|
child: _buildAktifRentalCard(rental),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAktifRentalCard(Map<String, dynamic> rental) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
// Header section with status
|
||||||
Icons.play_circle_filled,
|
Container(
|
||||||
size: 80,
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.info.withOpacity(0.1),
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(16),
|
||||||
|
topRight: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.play_circle_fill, size: 18, color: AppColors.info),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
rental['status'] ?? 'AKTIF',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.info,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Asset details
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Image.network(
|
||||||
|
rental['imageUrl'] ?? '',
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder:
|
||||||
|
(context, error, stackTrace) => Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
color: Colors.grey[200],
|
||||||
|
child: const Icon(
|
||||||
|
Icons.broken_image,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'Tab Aktif',
|
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Konten tab Aktif akan ditampilkan di sini',
|
rental['name'] ?? 'Aset',
|
||||||
style: TextStyle(color: Colors.grey),
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildInfoRow(
|
||||||
|
icon: Icons.inventory_2_outlined,
|
||||||
|
text: '${rental['jumlahUnit'] ?? 0} Unit',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildInfoRow(
|
||||||
|
icon: Icons.calendar_today_outlined,
|
||||||
|
text: rental['tanggalSewa'] ?? '',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildInfoRow(
|
||||||
|
icon: Icons.schedule,
|
||||||
|
text: rental['rentangWaktu'] ?? '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Divider(height: 1, thickness: 1, color: Colors.grey.shade100),
|
||||||
|
// Price section
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Total',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
rental['totalPrice'] ?? 'Rp 0',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildActionButton(
|
||||||
|
icon: Icons.info_outline,
|
||||||
|
label: 'Lihat Detail',
|
||||||
|
onPressed: () => controller.viewRentalDetail(rental),
|
||||||
|
iconColor: AppColors.info,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBelumBayarTab() {
|
Widget _buildBelumBayarTab() {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
// Show loading indicator while fetching data
|
// Show loading indicator while fetching data
|
||||||
if (controller.isLoading.value) {
|
if (controller.isLoading.value) {
|
||||||
return const Center(
|
return const Center(child: CircularProgressIndicator());
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there is any data to display
|
// Check if there is any data to display
|
||||||
@ -232,12 +379,16 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Build a card for each rental item
|
// Build a card for each rental item
|
||||||
...controller.rentals.map((rental) => Column(
|
...controller.rentals
|
||||||
|
.map(
|
||||||
|
(rental) => Column(
|
||||||
children: [
|
children: [
|
||||||
_buildUnpaidRentalCard(rental),
|
_buildUnpaidRentalCard(rental),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
)).toList(),
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
_buildTipsSection(),
|
_buildTipsSection(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -259,7 +410,8 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
Widget _buildUnpaidRentalCard(Map<String, dynamic> rental) {
|
Widget _buildUnpaidRentalCard(Map<String, dynamic> rental) {
|
||||||
// Determine status color based on status
|
// Determine status color based on status
|
||||||
final bool isPembayaranDenda = rental['status'] == 'PEMBAYARAN DENDA';
|
final bool isPembayaranDenda = rental['status'] == 'PEMBAYARAN DENDA';
|
||||||
final Color statusColor = isPembayaranDenda ? AppColors.error : AppColors.warning;
|
final Color statusColor =
|
||||||
|
isPembayaranDenda ? AppColors.error : AppColors.warning;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -289,7 +441,9 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
isPembayaranDenda ? Icons.warning_amber_rounded : Icons.access_time_rounded,
|
isPembayaranDenda
|
||||||
|
? Icons.warning_amber_rounded
|
||||||
|
: Icons.access_time_rounded,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: statusColor,
|
color: statusColor,
|
||||||
),
|
),
|
||||||
@ -315,7 +469,9 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
// Asset image with rounded corners
|
// Asset image with rounded corners
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: rental['imageUrl'] != null && rental['imageUrl'].toString().startsWith('http')
|
child:
|
||||||
|
rental['imageUrl'] != null &&
|
||||||
|
rental['imageUrl'].toString().startsWith('http')
|
||||||
? Image.network(
|
? Image.network(
|
||||||
rental['imageUrl'],
|
rental['imageUrl'],
|
||||||
width: 90,
|
width: 90,
|
||||||
@ -338,7 +494,8 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
: Image.asset(
|
: Image.asset(
|
||||||
rental['imageUrl'] ?? 'assets/images/gambar_pendukung.jpg',
|
rental['imageUrl'] ??
|
||||||
|
'assets/images/gambar_pendukung.jpg',
|
||||||
width: 90,
|
width: 90,
|
||||||
height: 90,
|
height: 90,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@ -389,39 +546,11 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
text: rental['rentangWaktu'] ?? '',
|
text: rental['rentangWaktu'] ?? '',
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// Countdown timer
|
if ((rental['status'] ?? '').toString().toUpperCase() ==
|
||||||
Container(
|
'MENUNGGU PEMBAYARAN' &&
|
||||||
padding: const EdgeInsets.symmetric(
|
rental['updated_at'] != null)
|
||||||
horizontal: 10,
|
CountdownTimerWidget(
|
||||||
vertical: 6,
|
updatedAt: DateTime.parse(rental['updated_at']),
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.error.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color: AppColors.error.withOpacity(0.3),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.timer_outlined,
|
|
||||||
size: 14,
|
|
||||||
color: AppColors.error,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'Bayar dalam ${rental['countdown'] ?? '00:59:59'}',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColors.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -465,7 +594,10 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: rental['status'] == 'PEMBAYARAN DENDA' ? AppColors.error : AppColors.warning,
|
backgroundColor:
|
||||||
|
rental['status'] == 'PEMBAYARAN DENDA'
|
||||||
|
? AppColors.error
|
||||||
|
: AppColors.warning,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@ -477,8 +609,13 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
rental['status'] == 'PEMBAYARAN DENDA' ? 'Bayar Denda' : 'Bayar Sekarang',
|
rental['status'] == 'PEMBAYARAN DENDA'
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
? 'Bayar Denda'
|
||||||
|
: 'Bayar Sekarang',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -502,6 +639,10 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
if ((rental['status'] ?? '').toString().toUpperCase() !=
|
||||||
|
'PEMBAYARAN DENDA' &&
|
||||||
|
(rental['status'] ?? '').toString().toUpperCase() !=
|
||||||
|
'PERIKSA PEMBAYARAN DENDA')
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildActionButton(
|
child: _buildActionButton(
|
||||||
icon: Icons.cancel_outlined,
|
icon: Icons.cancel_outlined,
|
||||||
@ -560,9 +701,7 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
return Obx(() {
|
return Obx(() {
|
||||||
// Show loading indicator while fetching data
|
// Show loading indicator while fetching data
|
||||||
if (controller.isLoadingAccepted.value) {
|
if (controller.isLoadingAccepted.value) {
|
||||||
return const Center(
|
return const Center(child: CircularProgressIndicator());
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there is any data to display
|
// Check if there is any data to display
|
||||||
@ -573,12 +712,16 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Build a card for each accepted rental item
|
// Build a card for each accepted rental item
|
||||||
...controller.acceptedRentals.map((rental) => Column(
|
...controller.acceptedRentals
|
||||||
|
.map(
|
||||||
|
(rental) => Column(
|
||||||
children: [
|
children: [
|
||||||
_buildDiterimaRentalCard(rental),
|
_buildDiterimaRentalCard(rental),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
)).toList(),
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
_buildTipsSectionDiterima(),
|
_buildTipsSectionDiterima(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -652,7 +795,9 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
// Asset image with rounded corners
|
// Asset image with rounded corners
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: rental['imageUrl'] != null && rental['imageUrl'].toString().startsWith('http')
|
child:
|
||||||
|
rental['imageUrl'] != null &&
|
||||||
|
rental['imageUrl'].toString().startsWith('http')
|
||||||
? Image.network(
|
? Image.network(
|
||||||
rental['imageUrl'],
|
rental['imageUrl'],
|
||||||
width: 90,
|
width: 90,
|
||||||
@ -675,7 +820,8 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
: Image.asset(
|
: Image.asset(
|
||||||
rental['imageUrl'] ?? 'assets/images/gambar_pendukung.jpg',
|
rental['imageUrl'] ??
|
||||||
|
'assets/images/gambar_pendukung.jpg',
|
||||||
width: 90,
|
width: 90,
|
||||||
height: 90,
|
height: 90,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@ -826,11 +972,14 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: controller.completedRentals
|
children:
|
||||||
.map((rental) => Padding(
|
controller.completedRentals
|
||||||
|
.map(
|
||||||
|
(rental) => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
child: _buildSelesaiRentalCard(rental),
|
child: _buildSelesaiRentalCard(rental),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -888,7 +1037,9 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
// Asset image with rounded corners
|
// Asset image with rounded corners
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: rental['imageUrl'] != null && rental['imageUrl'].toString().startsWith('http')
|
child:
|
||||||
|
rental['imageUrl'] != null &&
|
||||||
|
rental['imageUrl'].toString().startsWith('http')
|
||||||
? Image.network(
|
? Image.network(
|
||||||
rental['imageUrl'],
|
rental['imageUrl'],
|
||||||
width: 90,
|
width: 90,
|
||||||
@ -911,7 +1062,8 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
: Image.asset(
|
: Image.asset(
|
||||||
rental['imageUrl'] ?? 'assets/images/gambar_pendukung.jpg',
|
rental['imageUrl'] ??
|
||||||
|
'assets/images/gambar_pendukung.jpg',
|
||||||
width: 90,
|
width: 90,
|
||||||
height: 90,
|
height: 90,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@ -972,11 +1124,7 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Divider
|
// Divider
|
||||||
Divider(
|
Divider(color: Colors.grey.shade200, thickness: 1, height: 1),
|
||||||
color: Colors.grey.shade200,
|
|
||||||
thickness: 1,
|
|
||||||
height: 1,
|
|
||||||
),
|
|
||||||
|
|
||||||
// Price section
|
// Price section
|
||||||
Padding(
|
Padding(
|
||||||
@ -1047,11 +1195,14 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: controller.cancelledRentals
|
children:
|
||||||
.map((rental) => Padding(
|
controller.cancelledRentals
|
||||||
|
.map(
|
||||||
|
(rental) => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
child: _buildDibatalkanRentalCard(rental),
|
child: _buildDibatalkanRentalCard(rental),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -1109,7 +1260,9 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
// Asset image with rounded corners
|
// Asset image with rounded corners
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: rental['imageUrl'] != null && rental['imageUrl'].toString().startsWith('http')
|
child:
|
||||||
|
rental['imageUrl'] != null &&
|
||||||
|
rental['imageUrl'].toString().startsWith('http')
|
||||||
? Image.network(
|
? Image.network(
|
||||||
rental['imageUrl'],
|
rental['imageUrl'],
|
||||||
width: 90,
|
width: 90,
|
||||||
@ -1132,7 +1285,8 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
: Image.asset(
|
: Image.asset(
|
||||||
rental['imageUrl'] ?? 'assets/images/gambar_pendukung.jpg',
|
rental['imageUrl'] ??
|
||||||
|
'assets/images/gambar_pendukung.jpg',
|
||||||
width: 90,
|
width: 90,
|
||||||
height: 90,
|
height: 90,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@ -1185,7 +1339,8 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
icon: Icons.access_time,
|
icon: Icons.access_time,
|
||||||
text: rental['duration'] ?? '-',
|
text: rental['duration'] ?? '-',
|
||||||
),
|
),
|
||||||
if (rental['alasanPembatalan'] != null && rental['alasanPembatalan'] != '-')
|
if (rental['alasanPembatalan'] != null &&
|
||||||
|
rental['alasanPembatalan'] != '-')
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: _buildInfoRow(
|
child: _buildInfoRow(
|
||||||
@ -1201,11 +1356,7 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Divider
|
// Divider
|
||||||
Divider(
|
Divider(color: Colors.grey.shade200, thickness: 1, height: 1),
|
||||||
color: Colors.grey.shade200,
|
|
||||||
thickness: 1,
|
|
||||||
height: 1,
|
|
||||||
),
|
|
||||||
|
|
||||||
// Price section
|
// Price section
|
||||||
Padding(
|
Padding(
|
||||||
@ -1243,15 +1394,6 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
iconColor: AppColors.info,
|
iconColor: AppColors.info,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: _buildActionButton(
|
|
||||||
icon: Icons.refresh,
|
|
||||||
label: 'Pesan Kembali',
|
|
||||||
onPressed: () {},
|
|
||||||
iconColor: AppColors.success,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1490,4 +1632,261 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildDikembalikanTab() {
|
||||||
|
return Obx(() {
|
||||||
|
if (controller.isLoadingReturned.value) {
|
||||||
|
return const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(20.0),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (controller.returnedRentals.isEmpty) {
|
||||||
|
return _buildTabContent(
|
||||||
|
icon: Icons.assignment_return,
|
||||||
|
title: 'Belum Ada Sewa Dikembalikan',
|
||||||
|
subtitle: 'Sewa yang sudah dikembalikan akan muncul di sini',
|
||||||
|
buttonText: 'Lihat Aset',
|
||||||
|
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||||
|
color: Colors.deepPurple,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SingleChildScrollView(
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
children:
|
||||||
|
controller.returnedRentals
|
||||||
|
.map(
|
||||||
|
(rental) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
|
child: _buildDikembalikanRentalCard(rental),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDikembalikanRentalCard(Map<String, dynamic> rental) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Header section with status
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.info.withOpacity(0.1),
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(16),
|
||||||
|
topRight: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.assignment_return, size: 18, color: AppColors.info),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
rental['status'] ?? 'DIKEMBALIKAN',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.info,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Asset details
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Image.network(
|
||||||
|
rental['imageUrl'] ?? '',
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder:
|
||||||
|
(context, error, stackTrace) => Container(
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
color: Colors.grey[200],
|
||||||
|
child: const Icon(Icons.image, color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
rental['name'] ?? 'Aset',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildInfoRow(
|
||||||
|
icon: Icons.inventory_2_outlined,
|
||||||
|
text: '${rental['jumlahUnit'] ?? 0} Unit',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildInfoRow(
|
||||||
|
icon: Icons.calendar_today_outlined,
|
||||||
|
text: rental['tanggalSewa'] ?? '',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildInfoRow(
|
||||||
|
icon: Icons.schedule,
|
||||||
|
text: rental['rentangWaktu'] ?? '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Divider(height: 1, thickness: 1, color: Colors.grey.shade100),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Total',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
rental['totalPrice'] ?? 'Rp 0',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: _buildActionButton(
|
||||||
|
icon: Icons.info_outline,
|
||||||
|
label: 'Lihat Detail',
|
||||||
|
onPressed: () => controller.viewRentalDetail(rental),
|
||||||
|
iconColor: AppColors.info,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CountdownTimerWidget extends StatefulWidget {
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final VoidCallback? onTimeout;
|
||||||
|
const CountdownTimerWidget({
|
||||||
|
required this.updatedAt,
|
||||||
|
this.onTimeout,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
@override
|
||||||
|
State<CountdownTimerWidget> createState() => _CountdownTimerWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
|
||||||
|
late Duration remaining;
|
||||||
|
Timer? timer;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
updateRemaining();
|
||||||
|
timer = Timer.periodic(
|
||||||
|
const Duration(seconds: 1),
|
||||||
|
(_) => updateRemaining(),
|
||||||
|
);
|
||||||
|
print('DEBUG updated_at: ${widget.updatedAt}');
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateRemaining() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final deadline = widget.updatedAt.add(const Duration(hours: 1));
|
||||||
|
setState(() {
|
||||||
|
remaining = deadline.difference(now);
|
||||||
|
if (remaining.isNegative) {
|
||||||
|
remaining = Duration.zero;
|
||||||
|
timer?.cancel();
|
||||||
|
widget.onTimeout?.call();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (remaining.inSeconds <= 0) {
|
||||||
|
return Text('Waktu habis', style: TextStyle(color: Colors.red));
|
||||||
|
}
|
||||||
|
final h = remaining.inHours;
|
||||||
|
final m = remaining.inMinutes % 60;
|
||||||
|
final s = remaining.inSeconds % 60;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.withOpacity(0.08),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.red.withOpacity(0.3), width: 1),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.timer_outlined, size: 14, color: Colors.red),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Bayar dalam ${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 'navigation_service.dart';
|
||||||
import '../data/providers/auth_provider.dart';
|
import '../data/providers/auth_provider.dart';
|
||||||
import '../modules/warga/controllers/warga_dashboard_controller.dart';
|
import '../modules/warga/controllers/warga_dashboard_controller.dart';
|
||||||
|
import '../data/providers/aset_provider.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
/// Abstract class untuk mengelola lifecycle service dan dependency
|
/// Abstract class untuk mengelola lifecycle service dan dependency
|
||||||
abstract class ServiceManager {
|
abstract class ServiceManager {
|
||||||
@ -26,6 +28,11 @@ abstract class ServiceManager {
|
|||||||
Get.put(AuthProvider(), permanent: true);
|
Get.put(AuthProvider(), permanent: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register AsetProvider if not already registered
|
||||||
|
if (!Get.isRegistered<AsetProvider>()) {
|
||||||
|
Get.put(AsetProvider(), permanent: true);
|
||||||
|
}
|
||||||
|
|
||||||
// Register WargaDashboardController as a permanent controller
|
// Register WargaDashboardController as a permanent controller
|
||||||
// This ensures it's always available for the drawer
|
// This ensures it's always available for the drawer
|
||||||
registerWargaDashboardController();
|
registerWargaDashboardController();
|
||||||
|
|||||||
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:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'app_colors.dart';
|
import 'app_colors.dart';
|
||||||
|
|
||||||
/// App theme configuration
|
/// App theme configuration
|
||||||
@ -19,13 +20,16 @@ class AppTheme {
|
|||||||
),
|
),
|
||||||
scaffoldBackgroundColor: AppColors.background,
|
scaffoldBackgroundColor: AppColors.background,
|
||||||
|
|
||||||
|
// Set Lato as the default font for the entire app
|
||||||
|
fontFamily: GoogleFonts.lato().fontFamily,
|
||||||
|
|
||||||
// App bar theme
|
// App bar theme
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
backgroundColor: AppColors.primary,
|
backgroundColor: AppColors.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
iconTheme: const IconThemeData(color: Colors.white),
|
iconTheme: const IconThemeData(color: Colors.white),
|
||||||
titleTextStyle: const TextStyle(
|
titleTextStyle: GoogleFonts.lato(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@ -50,7 +54,10 @@ class AppTheme {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
textStyle: GoogleFonts.lato(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -62,7 +69,10 @@ class AppTheme {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
textStyle: GoogleFonts.lato(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -70,7 +80,10 @@ class AppTheme {
|
|||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: AppColors.primary,
|
foregroundColor: AppColors.primary,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||||
textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
textStyle: GoogleFonts.lato(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -98,8 +111,8 @@ class AppTheme {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(color: AppColors.error, width: 1.5),
|
borderSide: BorderSide(color: AppColors.error, width: 1.5),
|
||||||
),
|
),
|
||||||
hintStyle: TextStyle(color: AppColors.textLight),
|
hintStyle: GoogleFonts.lato(color: AppColors.textLight),
|
||||||
labelStyle: TextStyle(color: AppColors.textSecondary),
|
labelStyle: GoogleFonts.lato(color: AppColors.textSecondary),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Checkbox theme
|
// Checkbox theme
|
||||||
@ -115,21 +128,21 @@ class AppTheme {
|
|||||||
|
|
||||||
// Text themes
|
// Text themes
|
||||||
textTheme: TextTheme(
|
textTheme: TextTheme(
|
||||||
displayLarge: TextStyle(color: AppColors.textPrimary),
|
displayLarge: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||||
displayMedium: TextStyle(color: AppColors.textPrimary),
|
displayMedium: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||||
displaySmall: TextStyle(color: AppColors.textPrimary),
|
displaySmall: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||||
headlineLarge: TextStyle(color: AppColors.textPrimary),
|
headlineLarge: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||||
headlineMedium: TextStyle(color: AppColors.textPrimary),
|
headlineMedium: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||||
headlineSmall: TextStyle(color: AppColors.textPrimary),
|
headlineSmall: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||||
titleLarge: TextStyle(color: AppColors.textPrimary),
|
titleLarge: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||||
titleMedium: TextStyle(color: AppColors.textPrimary),
|
titleMedium: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||||
titleSmall: TextStyle(color: AppColors.textPrimary),
|
titleSmall: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||||
bodyLarge: TextStyle(color: AppColors.textPrimary),
|
bodyLarge: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||||
bodyMedium: TextStyle(color: AppColors.textPrimary),
|
bodyMedium: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||||
bodySmall: TextStyle(color: AppColors.textSecondary),
|
bodySmall: GoogleFonts.lato(color: AppColors.textSecondary),
|
||||||
labelLarge: TextStyle(color: AppColors.textPrimary),
|
labelLarge: GoogleFonts.lato(color: AppColors.textPrimary),
|
||||||
labelMedium: TextStyle(color: AppColors.textSecondary),
|
labelMedium: GoogleFonts.lato(color: AppColors.textSecondary),
|
||||||
labelSmall: TextStyle(color: AppColors.textLight),
|
labelSmall: GoogleFonts.lato(color: AppColors.textLight),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Divider theme
|
// Divider theme
|
||||||
|
|||||||
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
|
// Settings Items
|
||||||
_buildDrawerItem(
|
|
||||||
icon: Icons.info_outline_rounded,
|
|
||||||
title: 'Tentang Aplikasi',
|
|
||||||
subtitle: 'Informasi dan bantuan',
|
|
||||||
showTrailing: false,
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
// Show about dialog
|
|
||||||
showAboutDialog(
|
|
||||||
context: context,
|
|
||||||
applicationName: 'BumRent App',
|
|
||||||
applicationVersion: '1.0.0',
|
|
||||||
applicationIcon: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Image.asset(
|
|
||||||
'assets/images/logo.png',
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'Aplikasi penyewaan dan berlangganan aset milik BUMDes untuk warga desa.',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_buildDrawerItem(
|
_buildDrawerItem(
|
||||||
icon: Icons.logout_rounded,
|
icon: Icons.logout_rounded,
|
||||||
title: 'Keluar',
|
title: 'Keluar',
|
||||||
|
|||||||
@ -525,6 +525,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
version: "5.1.1"
|
||||||
|
logger:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: logger
|
||||||
|
sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.0"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -48,6 +48,7 @@ dependencies:
|
|||||||
flutter_dotenv: ^5.1.0
|
flutter_dotenv: ^5.1.0
|
||||||
image_picker: ^1.0.7
|
image_picker: ^1.0.7
|
||||||
intl: 0.19.0
|
intl: 0.19.0
|
||||||
|
logger: ^2.1.0
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
get_storage: ^2.1.1
|
get_storage: ^2.1.1
|
||||||
|
|||||||
30
widget_test.dart
Normal file
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