first commit
This commit is contained in:
25
lib/app/bindings/auth_binding.dart
Normal file
25
lib/app/bindings/auth_binding.dart
Normal file
@ -0,0 +1,25 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../data/providers/auth_provider.dart';
|
||||
import '../modules/auth/controllers/auth_controller.dart';
|
||||
|
||||
class AuthBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
debugPrint('Initializing AuthBinding dependencies');
|
||||
|
||||
// Pastikan AuthProvider dibuat sekali dan bersifat permanen
|
||||
if (!Get.isRegistered<AuthProvider>()) {
|
||||
debugPrint('Registering AuthProvider in AuthBinding');
|
||||
Get.put<AuthProvider>(AuthProvider(), permanent: true);
|
||||
} else {
|
||||
debugPrint('AuthProvider already registered');
|
||||
}
|
||||
|
||||
// Buat AuthController
|
||||
debugPrint('Creating AuthController');
|
||||
Get.lazyPut<AuthController>(() => AuthController());
|
||||
|
||||
debugPrint('AuthBinding dependencies initialized');
|
||||
}
|
||||
}
|
||||
1
lib/app/bindings/home_binding.dart
Normal file
1
lib/app/bindings/home_binding.dart
Normal file
@ -0,0 +1 @@
|
||||
|
||||
30
lib/app/bindings/petugas_bumdes_binding.dart
Normal file
30
lib/app/bindings/petugas_bumdes_binding.dart
Normal file
@ -0,0 +1,30 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../data/providers/auth_provider.dart';
|
||||
import '../modules/petugas_bumdes/controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
|
||||
class PetugasBumdesBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Pastikan AuthProvider teregistrasi
|
||||
if (!Get.isRegistered<AuthProvider>()) {
|
||||
Get.put(AuthProvider());
|
||||
}
|
||||
|
||||
// Hapus terlebih dahulu untuk memastikan clean state
|
||||
try {
|
||||
if (Get.isRegistered<PetugasBumdesDashboardController>()) {
|
||||
Get.delete<PetugasBumdesDashboardController>(force: true);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error removing controller: $e');
|
||||
}
|
||||
|
||||
// Gunakan put untuk memastikan controller selalu tersedia dan permanent
|
||||
Get.put<PetugasBumdesDashboardController>(
|
||||
PetugasBumdesDashboardController(),
|
||||
permanent: true,
|
||||
);
|
||||
|
||||
print('✅ PetugasBumdesDashboardController registered successfully');
|
||||
}
|
||||
}
|
||||
13
lib/app/bindings/petugas_mitra_binding.dart
Normal file
13
lib/app/bindings/petugas_mitra_binding.dart
Normal file
@ -0,0 +1,13 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../data/providers/auth_provider.dart';
|
||||
import '../modules/petugas_mitra/controllers/petugas_mitra_dashboard_controller.dart';
|
||||
|
||||
class PetugasMitraBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<AuthProvider>(() => AuthProvider());
|
||||
Get.lazyPut<PetugasMitraDashboardController>(
|
||||
() => PetugasMitraDashboardController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
1
lib/app/bindings/profile_binding.dart
Normal file
1
lib/app/bindings/profile_binding.dart
Normal file
@ -0,0 +1 @@
|
||||
|
||||
25
lib/app/bindings/splash_binding.dart
Normal file
25
lib/app/bindings/splash_binding.dart
Normal file
@ -0,0 +1,25 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../data/providers/auth_provider.dart';
|
||||
import '../modules/splash/controllers/splash_controller.dart';
|
||||
|
||||
class SplashBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
debugPrint('Initializing SplashBinding dependencies');
|
||||
|
||||
// Pastikan AuthProvider dibuat sekali dan bersifat permanen
|
||||
if (!Get.isRegistered<AuthProvider>()) {
|
||||
debugPrint('Registering AuthProvider in SplashBinding');
|
||||
Get.put<AuthProvider>(AuthProvider(), permanent: true);
|
||||
} else {
|
||||
debugPrint('AuthProvider already registered');
|
||||
}
|
||||
|
||||
// Buat SplashController
|
||||
debugPrint('Creating SplashController');
|
||||
Get.put<SplashController>(SplashController());
|
||||
|
||||
debugPrint('SplashBinding dependencies initialized');
|
||||
}
|
||||
}
|
||||
11
lib/app/bindings/warga_binding.dart
Normal file
11
lib/app/bindings/warga_binding.dart
Normal file
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../data/providers/auth_provider.dart';
|
||||
import '../modules/warga/controllers/warga_dashboard_controller.dart';
|
||||
|
||||
class WargaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<AuthProvider>(() => AuthProvider());
|
||||
Get.lazyPut<WargaDashboardController>(() => WargaDashboardController());
|
||||
}
|
||||
}
|
||||
1
lib/app/core/theme/app_theme.dart
Normal file
1
lib/app/core/theme/app_theme.dart
Normal file
@ -0,0 +1 @@
|
||||
|
||||
84
lib/app/data/models/aset_model.dart
Normal file
84
lib/app/data/models/aset_model.dart
Normal file
@ -0,0 +1,84 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class AsetModel {
|
||||
final String id;
|
||||
final String nama;
|
||||
final String deskripsi;
|
||||
final String kategori;
|
||||
final int harga;
|
||||
final int? denda;
|
||||
final String status;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final int? kuantitas;
|
||||
final int? kuantitasTerpakai;
|
||||
final String? satuanUkur;
|
||||
|
||||
// Untuk menampung URL gambar pertama dari tabel foto_aset
|
||||
String? imageUrl;
|
||||
|
||||
// Menggunakan RxList untuk membuatnya mutable dan reaktif
|
||||
RxList<Map<String, dynamic>> satuanWaktuSewa = <Map<String, dynamic>>[].obs;
|
||||
|
||||
AsetModel({
|
||||
required this.id,
|
||||
required this.nama,
|
||||
required this.deskripsi,
|
||||
required this.kategori,
|
||||
required this.harga,
|
||||
this.denda,
|
||||
required this.status,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.kuantitas,
|
||||
this.kuantitasTerpakai,
|
||||
this.satuanUkur,
|
||||
this.imageUrl,
|
||||
List<Map<String, dynamic>>? initialSatuanWaktuSewa,
|
||||
}) {
|
||||
// Inisialisasi satuanWaktuSewa jika ada data awal
|
||||
if (initialSatuanWaktuSewa != null) {
|
||||
satuanWaktuSewa.addAll(initialSatuanWaktuSewa);
|
||||
}
|
||||
}
|
||||
|
||||
factory AsetModel.fromJson(Map<String, dynamic> json) {
|
||||
return AsetModel(
|
||||
id: json['id'] ?? '',
|
||||
nama: json['nama'] ?? '',
|
||||
deskripsi: json['deskripsi'] ?? '',
|
||||
kategori: json['kategori'] ?? '',
|
||||
harga: json['harga'] ?? 0,
|
||||
denda: json['denda'],
|
||||
status: json['status'] ?? '',
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
kuantitas: json['kuantitas'],
|
||||
kuantitasTerpakai: json['kuantitas_terpakai'],
|
||||
satuanUkur: json['satuan_ukur'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'nama': nama,
|
||||
'deskripsi': deskripsi,
|
||||
'kategori': kategori,
|
||||
'harga': harga,
|
||||
'denda': denda,
|
||||
'status': status,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
'kuantitas': kuantitas,
|
||||
'kuantitas_terpakai': kuantitasTerpakai,
|
||||
'satuan_ukur': satuanUkur,
|
||||
};
|
||||
}
|
||||
}
|
||||
41
lib/app/data/models/foto_aset_model.dart
Normal file
41
lib/app/data/models/foto_aset_model.dart
Normal file
@ -0,0 +1,41 @@
|
||||
class FotoAsetModel {
|
||||
final String id;
|
||||
final String fotoAset; // URL foto
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String idAset;
|
||||
|
||||
FotoAsetModel({
|
||||
required this.id,
|
||||
required this.fotoAset,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
required this.idAset,
|
||||
});
|
||||
|
||||
factory FotoAsetModel.fromJson(Map<String, dynamic> json) {
|
||||
return FotoAsetModel(
|
||||
id: json['id'] ?? '',
|
||||
fotoAset: json['foto_aset'] ?? '',
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
idAset: json['id_aset'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'foto_aset': fotoAset,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
'id_aset': idAset,
|
||||
};
|
||||
}
|
||||
}
|
||||
54
lib/app/data/models/paket_model.dart
Normal file
54
lib/app/data/models/paket_model.dart
Normal file
@ -0,0 +1,54 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class PaketModel {
|
||||
final String? id;
|
||||
final String? nama;
|
||||
final String? deskripsi;
|
||||
final int? harga;
|
||||
final int? kuantitas;
|
||||
final String? foto_paket;
|
||||
final List<dynamic>? satuanWaktuSewa;
|
||||
|
||||
PaketModel({
|
||||
this.id,
|
||||
this.nama,
|
||||
this.deskripsi,
|
||||
this.harga,
|
||||
this.kuantitas,
|
||||
this.foto_paket,
|
||||
this.satuanWaktuSewa,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'nama': nama,
|
||||
'deskripsi': deskripsi,
|
||||
'harga': harga,
|
||||
'kuantitas': kuantitas,
|
||||
'foto_paket': foto_paket,
|
||||
'satuanWaktuSewa': satuanWaktuSewa,
|
||||
};
|
||||
}
|
||||
|
||||
factory PaketModel.fromMap(Map<String, dynamic> map) {
|
||||
return PaketModel(
|
||||
id: map['id'],
|
||||
nama: map['nama'],
|
||||
deskripsi: map['deskripsi'],
|
||||
harga: map['harga']?.toInt(),
|
||||
kuantitas: map['kuantitas']?.toInt(),
|
||||
foto_paket: map['foto_paket'],
|
||||
satuanWaktuSewa: map['satuanWaktuSewa'],
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory PaketModel.fromJson(String source) => PaketModel.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PaketModel(id: $id, nama: $nama, deskripsi: $deskripsi, harga: $harga, kuantitas: $kuantitas, foto_paket: $foto_paket, satuanWaktuSewa: $satuanWaktuSewa)';
|
||||
}
|
||||
}
|
||||
79
lib/app/data/models/pesanan_model.dart
Normal file
79
lib/app/data/models/pesanan_model.dart
Normal file
@ -0,0 +1,79 @@
|
||||
class PesananModel {
|
||||
final String id;
|
||||
final String asetId;
|
||||
final String satuanWaktuId;
|
||||
final String userId;
|
||||
final String status;
|
||||
final DateTime tanggalPemesanan;
|
||||
final String jamPemesanan;
|
||||
final int durasi;
|
||||
final int totalHarga;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
// Optional fields for joined data from other tables
|
||||
String? namaSatuanWaktu;
|
||||
String? namaAset;
|
||||
String? namaUser;
|
||||
|
||||
PesananModel({
|
||||
required this.id,
|
||||
required this.asetId,
|
||||
required this.satuanWaktuId,
|
||||
required this.userId,
|
||||
required this.status,
|
||||
required this.tanggalPemesanan,
|
||||
required this.jamPemesanan,
|
||||
required this.durasi,
|
||||
required this.totalHarga,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.namaSatuanWaktu,
|
||||
this.namaAset,
|
||||
this.namaUser,
|
||||
});
|
||||
|
||||
factory PesananModel.fromJson(Map<String, dynamic> json) {
|
||||
return PesananModel(
|
||||
id: json['id'] ?? '',
|
||||
asetId: json['aset_id'] ?? '',
|
||||
satuanWaktuId: json['satuan_waktu_id'] ?? '',
|
||||
userId: json['user_id'] ?? '',
|
||||
status: json['status'] ?? 'pending',
|
||||
tanggalPemesanan:
|
||||
json['tanggal_pemesanan'] != null
|
||||
? DateTime.parse(json['tanggal_pemesanan'])
|
||||
: DateTime.now(),
|
||||
jamPemesanan: json['jam_pemesanan'] ?? '00:00',
|
||||
durasi: json['durasi'] ?? 1,
|
||||
totalHarga: json['total_harga'] ?? 0,
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
namaSatuanWaktu: json['nama_satuan_waktu'],
|
||||
namaAset: json['nama_aset'],
|
||||
namaUser: json['nama_user'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'aset_id': asetId,
|
||||
'satuan_waktu_id': satuanWaktuId,
|
||||
'user_id': userId,
|
||||
'status': status,
|
||||
'tanggal_pemesanan': tanggalPemesanan.toIso8601String().split('T')[0],
|
||||
'jam_pemesanan': jamPemesanan,
|
||||
'durasi': durasi,
|
||||
'total_harga': totalHarga,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
1
lib/app/data/models/rental_booking_model.dart
Normal file
1
lib/app/data/models/rental_booking_model.dart
Normal file
@ -0,0 +1 @@
|
||||
|
||||
102
lib/app/data/models/rental_item_model.dart
Normal file
102
lib/app/data/models/rental_item_model.dart
Normal file
@ -0,0 +1,102 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class RentalItem {
|
||||
final String id;
|
||||
final String title;
|
||||
final String description;
|
||||
final double pricePerDay;
|
||||
final String? imageUrl;
|
||||
final String ownerId;
|
||||
final String category;
|
||||
final List<String>? features;
|
||||
final bool isAvailable;
|
||||
final String? location;
|
||||
final DateTime createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
RentalItem({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.pricePerDay,
|
||||
this.imageUrl,
|
||||
required this.ownerId,
|
||||
required this.category,
|
||||
this.features,
|
||||
required this.isAvailable,
|
||||
this.location,
|
||||
required this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'price_per_day': pricePerDay,
|
||||
'image_url': imageUrl,
|
||||
'owner_id': ownerId,
|
||||
'category': category,
|
||||
'features': features,
|
||||
'is_available': isAvailable,
|
||||
'location': location,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
factory RentalItem.fromMap(Map<String, dynamic> map) {
|
||||
return RentalItem(
|
||||
id: map['id'] ?? '',
|
||||
title: map['title'] ?? '',
|
||||
description: map['description'] ?? '',
|
||||
pricePerDay: map['price_per_day']?.toDouble() ?? 0.0,
|
||||
imageUrl: map['image_url'],
|
||||
ownerId: map['owner_id'] ?? '',
|
||||
category: map['category'] ?? '',
|
||||
features:
|
||||
map['features'] != null ? List<String>.from(map['features']) : null,
|
||||
isAvailable: map['is_available'] ?? true,
|
||||
location: map['location'],
|
||||
createdAt: DateTime.parse(map['created_at']),
|
||||
updatedAt:
|
||||
map['updated_at'] != null ? DateTime.parse(map['updated_at']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory RentalItem.fromJson(String source) =>
|
||||
RentalItem.fromMap(json.decode(source));
|
||||
|
||||
RentalItem copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? description,
|
||||
double? pricePerDay,
|
||||
String? imageUrl,
|
||||
String? ownerId,
|
||||
String? category,
|
||||
List<String>? features,
|
||||
bool? isAvailable,
|
||||
String? location,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return RentalItem(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
pricePerDay: pricePerDay ?? this.pricePerDay,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
category: category ?? this.category,
|
||||
features: features ?? this.features,
|
||||
isAvailable: isAvailable ?? this.isAvailable,
|
||||
location: location ?? this.location,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
37
lib/app/data/models/satuan_waktu_model.dart
Normal file
37
lib/app/data/models/satuan_waktu_model.dart
Normal file
@ -0,0 +1,37 @@
|
||||
class SatuanWaktuModel {
|
||||
final String id;
|
||||
final String namaSatuanWaktu;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
SatuanWaktuModel({
|
||||
required this.id,
|
||||
required this.namaSatuanWaktu,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
factory SatuanWaktuModel.fromJson(Map<String, dynamic> json) {
|
||||
return SatuanWaktuModel(
|
||||
id: json['id'] ?? '',
|
||||
namaSatuanWaktu: json['nama_satuan_waktu'] ?? '',
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'nama_satuan_waktu': namaSatuanWaktu,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
53
lib/app/data/models/satuan_waktu_sewa_model.dart
Normal file
53
lib/app/data/models/satuan_waktu_sewa_model.dart
Normal file
@ -0,0 +1,53 @@
|
||||
class SatuanWaktuSewaModel {
|
||||
final String id;
|
||||
final String asetId;
|
||||
final String satuanWaktuId;
|
||||
final int harga;
|
||||
final int? denda;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
// Untuk menyimpan nama satuan waktu (jam/hari) dari tabel satuan_waktu
|
||||
String? namaSatuanWaktu;
|
||||
|
||||
SatuanWaktuSewaModel({
|
||||
required this.id,
|
||||
required this.asetId,
|
||||
required this.satuanWaktuId,
|
||||
required this.harga,
|
||||
this.denda,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.namaSatuanWaktu,
|
||||
});
|
||||
|
||||
factory SatuanWaktuSewaModel.fromJson(Map<String, dynamic> json) {
|
||||
return SatuanWaktuSewaModel(
|
||||
id: json['id'] ?? '',
|
||||
asetId: json['aset_id'] ?? '',
|
||||
satuanWaktuId: json['satuan_waktu_id'] ?? '',
|
||||
harga: json['harga'] ?? 0,
|
||||
denda: json['denda'],
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'aset_id': asetId,
|
||||
'satuan_waktu_id': satuanWaktuId,
|
||||
'harga': harga,
|
||||
'denda': denda,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
71
lib/app/data/models/user_model.dart
Normal file
71
lib/app/data/models/user_model.dart
Normal file
@ -0,0 +1,71 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class User {
|
||||
final String id;
|
||||
final String email;
|
||||
final String? name;
|
||||
final String? avatarUrl;
|
||||
final String? phoneNumber;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
User({
|
||||
required this.id,
|
||||
required this.email,
|
||||
this.name,
|
||||
this.avatarUrl,
|
||||
this.phoneNumber,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'email': email,
|
||||
'name': name,
|
||||
'avatar_url': avatarUrl,
|
||||
'phone_number': phoneNumber,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
factory User.fromMap(Map<String, dynamic> map) {
|
||||
return User(
|
||||
id: map['id'] ?? '',
|
||||
email: map['email'] ?? '',
|
||||
name: map['name'],
|
||||
avatarUrl: map['avatar_url'],
|
||||
phoneNumber: map['phone_number'],
|
||||
createdAt:
|
||||
map['created_at'] != null ? DateTime.parse(map['created_at']) : null,
|
||||
updatedAt:
|
||||
map['updated_at'] != null ? DateTime.parse(map['updated_at']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory User.fromJson(String source) => User.fromMap(json.decode(source));
|
||||
|
||||
User copyWith({
|
||||
String? id,
|
||||
String? email,
|
||||
String? name,
|
||||
String? avatarUrl,
|
||||
String? phoneNumber,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return User(
|
||||
id: id ?? this.id,
|
||||
email: email ?? this.email,
|
||||
name: name ?? this.name,
|
||||
avatarUrl: avatarUrl ?? this.avatarUrl,
|
||||
phoneNumber: phoneNumber ?? this.phoneNumber,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
1099
lib/app/data/providers/aset_provider.dart
Normal file
1099
lib/app/data/providers/aset_provider.dart
Normal file
File diff suppressed because it is too large
Load Diff
535
lib/app/data/providers/auth_provider.dart
Normal file
535
lib/app/data/providers/auth_provider.dart
Normal file
@ -0,0 +1,535 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
|
||||
class AuthProvider extends GetxService {
|
||||
late final SupabaseClient client;
|
||||
bool _isInitialized = false;
|
||||
|
||||
Future<AuthProvider> init() async {
|
||||
// Cek jika sudah diinisialisasi sebelumnya
|
||||
if (_isInitialized) {
|
||||
debugPrint('Supabase already initialized');
|
||||
return this;
|
||||
}
|
||||
|
||||
try {
|
||||
// Cek jika dotenv sudah dimuat
|
||||
if (dotenv.env['SUPABASE_URL'] == null ||
|
||||
dotenv.env['SUPABASE_ANON_KEY'] == null) {
|
||||
await dotenv.load();
|
||||
}
|
||||
|
||||
final supabaseUrl = dotenv.env['SUPABASE_URL'];
|
||||
final supabaseKey = dotenv.env['SUPABASE_ANON_KEY'];
|
||||
|
||||
if (supabaseUrl == null || supabaseKey == null) {
|
||||
throw Exception('Supabase credentials not found in .env file');
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'Initializing Supabase with URL: ${supabaseUrl.substring(0, 15)}...',
|
||||
);
|
||||
|
||||
await Supabase.initialize(
|
||||
url: supabaseUrl,
|
||||
anonKey: supabaseKey,
|
||||
debug: true, // Aktifkan debugging untuk membantu troubleshooting
|
||||
);
|
||||
|
||||
client = Supabase.instance.client;
|
||||
_isInitialized = true;
|
||||
debugPrint('Supabase initialized successfully');
|
||||
return this;
|
||||
} catch (e) {
|
||||
debugPrint('Error initializing Supabase: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication methods
|
||||
Future<AuthResponse> signUp({
|
||||
required String email,
|
||||
required String password,
|
||||
Map<String, dynamic>? data,
|
||||
}) async {
|
||||
return await client.auth.signUp(
|
||||
email: email,
|
||||
password: password,
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
Future<AuthResponse> signIn({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
return await client.auth.signInWithPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> signOut() async {
|
||||
await client.auth.signOut();
|
||||
}
|
||||
|
||||
User? get currentUser => client.auth.currentUser;
|
||||
|
||||
Stream<AuthState> get authChanges => client.auth.onAuthStateChange;
|
||||
|
||||
String? getCurrentUserId() {
|
||||
try {
|
||||
final session = Supabase.instance.client.auth.currentSession;
|
||||
return session?.user.id;
|
||||
} catch (e) {
|
||||
print('Error getting current user ID: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk mendapatkan role_id dari raw_user_meta_data
|
||||
Future<String?> getUserRoleId() async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
debugPrint('No current user found when getting role');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('Fetching role_id from user metadata for user ID: ${user.id}');
|
||||
|
||||
// Cek user metadata untuk role_id
|
||||
final userMetadata = user.userMetadata;
|
||||
debugPrint('User metadata: $userMetadata');
|
||||
|
||||
// Cek beberapa kemungkinan nama field untuk role_id
|
||||
if (userMetadata != null) {
|
||||
if (userMetadata.containsKey('role_id')) {
|
||||
final roleId = userMetadata['role_id'].toString();
|
||||
debugPrint('Found role_id in metadata: $roleId');
|
||||
return roleId;
|
||||
}
|
||||
|
||||
if (userMetadata.containsKey('role')) {
|
||||
final role = userMetadata['role'].toString();
|
||||
debugPrint('Found role in metadata: $role');
|
||||
|
||||
// Coba konversi nama role ke UUID (from hardcoded data)
|
||||
if (role.toUpperCase() == 'WARGA') {
|
||||
return 'bb5360d5-8fd0-404e-8f6f-71ec4d8ad0ae';
|
||||
}
|
||||
if (role.toUpperCase() == 'PETUGAS_BUMDES') {
|
||||
return '38a8a23c-1873-4033-b977-3293247903b';
|
||||
}
|
||||
if (role.toUpperCase() == 'PETUGAS_MITRA') {
|
||||
return '8b1af754-0866-4e12-a9d8-da8ed31bec15';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Jika tidak ada di metadata, coba cari di tabel roles dengan user_id
|
||||
debugPrint('Checking roles table for user ID: ${user.id}');
|
||||
|
||||
try {
|
||||
// Mencoba mengambil roles berdasarkan id user di auth
|
||||
final roleData =
|
||||
await client
|
||||
.from('roles')
|
||||
.select('id')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
debugPrint('Role data by user_id: $roleData');
|
||||
|
||||
if (roleData != null && roleData.containsKey('id')) {
|
||||
final roleId = roleData['id'].toString();
|
||||
debugPrint('Found role ID in roles table: $roleId');
|
||||
return roleId;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error querying roles by user_id: $e');
|
||||
}
|
||||
|
||||
// Jika tidak ditemukan dengan user_id, coba lihat seluruh tabel roles
|
||||
// untuk debugging
|
||||
debugPrint('Getting all roles to debug matching issues');
|
||||
final allRoles = await client.from('roles').select('*').limit(10);
|
||||
|
||||
debugPrint('All roles in table: $allRoles');
|
||||
|
||||
// Fallback - tampaknya user belum di-assign role
|
||||
// Berikan hardcoded role berdasarkan email pattern
|
||||
final email = user.email?.toLowerCase();
|
||||
if (email != null) {
|
||||
if (email.contains('bumdes')) {
|
||||
return '38a8a23c-1873-4033-b977-3293247903b'; // PETUGAS_BUMDES
|
||||
} else if (email.contains('mitra')) {
|
||||
return '8b1af754-0866-4e12-a9d8-da8ed31bec15'; // PETUGAS_MITRA
|
||||
}
|
||||
}
|
||||
|
||||
// Default ke WARGA
|
||||
return 'bb5360d5-8fd0-404e-8f6f-71ec4d8ad0ae'; // WARGA
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching user role_id: $e');
|
||||
// Default ke WARGA sebagai fallback
|
||||
return 'bb5360d5-8fd0-404e-8f6f-71ec4d8ad0ae';
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk mendapatkan nama role dari tabel roles berdasarkan role_id
|
||||
Future<String?> getRoleName(String roleId) async {
|
||||
try {
|
||||
debugPrint('Fetching role name for role_id: $roleId');
|
||||
|
||||
// Ambil nama role dari tabel roles
|
||||
// ID di tabel roles adalah tipe UUID, pastikan format roleId sesuai
|
||||
final roleData =
|
||||
await client
|
||||
.from('roles')
|
||||
.select('nama_role, id')
|
||||
.eq('id', roleId)
|
||||
.maybeSingle();
|
||||
|
||||
debugPrint('Query result for roles table: $roleData');
|
||||
|
||||
if (roleData != null) {
|
||||
// Cek berbagai kemungkinan nama kolom
|
||||
String? roleName;
|
||||
if (roleData.containsKey('nama_role')) {
|
||||
roleName = roleData['nama_role'].toString();
|
||||
} else if (roleData.containsKey('nama_role')) {
|
||||
roleName = roleData['nama_role'].toString();
|
||||
} else if (roleData.containsKey('role_name')) {
|
||||
roleName = roleData['role_name'].toString();
|
||||
}
|
||||
|
||||
if (roleName != null) {
|
||||
debugPrint('Found role name in roles table: $roleName');
|
||||
return roleName;
|
||||
}
|
||||
|
||||
// Jika tidak ada nama kolom yang cocok, tampilkan kolom yang tersedia
|
||||
debugPrint(
|
||||
'Available columns in roles table: ${roleData.keys.join(', ')}',
|
||||
);
|
||||
}
|
||||
|
||||
// Lihat data lengkap tabel untuk troubleshooting
|
||||
debugPrint('Getting all roles data for troubleshooting');
|
||||
final allRoles = await client.from('roles').select('*').limit(5);
|
||||
|
||||
debugPrint('All roles table data (up to 5 rows): $allRoles');
|
||||
|
||||
// Hardcoded fallback berdasarkan UUID roleId yang dilihat dari data
|
||||
debugPrint('Using hardcoded fallback for role_id: $roleId');
|
||||
if (roleId == 'bb5360d5-8fd0-404e-8f6f-71ec4d8ad0ae') return 'WARGA';
|
||||
if (roleId == '38a8a23c-1873-4033-b977-3293247903b') {
|
||||
return 'PETUGAS_BUMDES';
|
||||
}
|
||||
if (roleId == '8b1af754-0866-4e12-a9d8-da8ed31bec15') {
|
||||
return 'PETUGAS_MITRA';
|
||||
}
|
||||
|
||||
// Default fallback jika role_id tidak dikenali
|
||||
debugPrint('Unrecognized role_id: $roleId, defaulting to WARGA');
|
||||
return 'WARGA';
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching role name: $e');
|
||||
return 'WARGA'; // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk mendapatkan nama lengkap dari tabel warga_desa berdasarkan user_id
|
||||
Future<String?> getUserFullName() async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
debugPrint('No current user found when getting full name');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('Fetching nama_lengkap for user_id: ${user.id}');
|
||||
|
||||
// Coba ambil nama lengkap dari tabel warga_desa
|
||||
final userData =
|
||||
await client
|
||||
.from('warga_desa')
|
||||
.select('nama_lengkap')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
debugPrint('User data from warga_desa table: $userData');
|
||||
|
||||
// Jika berhasil mendapatkan data
|
||||
if (userData != null && userData.containsKey('nama_lengkap')) {
|
||||
final namaLengkap = userData['nama_lengkap']?.toString();
|
||||
if (namaLengkap != null && namaLengkap.isNotEmpty) {
|
||||
debugPrint('Found nama_lengkap: $namaLengkap');
|
||||
return namaLengkap;
|
||||
}
|
||||
}
|
||||
|
||||
// Jika tidak ada data di warga_desa, coba cek struktur tabel untuk troubleshooting
|
||||
debugPrint('Checking warga_desa table structure');
|
||||
final tableData =
|
||||
await client.from('warga_desa').select('*').limit(1).maybeSingle();
|
||||
|
||||
if (tableData != null) {
|
||||
debugPrint(
|
||||
'Available columns in warga_desa table: ${tableData.keys.join(', ')}',
|
||||
);
|
||||
} else {
|
||||
debugPrint('No data found in warga_desa table');
|
||||
}
|
||||
|
||||
// Fallback ke data dari Supabase Auth
|
||||
final userMetadata = user.userMetadata;
|
||||
if (userMetadata != null) {
|
||||
if (userMetadata.containsKey('full_name')) {
|
||||
return userMetadata['full_name']?.toString();
|
||||
}
|
||||
if (userMetadata.containsKey('name')) {
|
||||
return userMetadata['name']?.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Gunakan email jika nama tidak ditemukan
|
||||
return user.email?.split('@').first ?? 'Pengguna Warga';
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching user full name: $e');
|
||||
return 'Pengguna Warga'; // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk mendapatkan avatar dari tabel warga_desa berdasarkan user_id
|
||||
Future<String?> getUserAvatar() async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
debugPrint('No current user found when getting avatar');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('Fetching avatar for user_id: ${user.id}');
|
||||
|
||||
// Coba ambil avatar dari tabel warga_desa
|
||||
final userData =
|
||||
await client
|
||||
.from('warga_desa')
|
||||
.select('avatar')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
debugPrint('Avatar data from warga_desa table: $userData');
|
||||
|
||||
// Jika berhasil mendapatkan data
|
||||
if (userData != null && userData.containsKey('avatar')) {
|
||||
final avatarUrl = userData['avatar']?.toString();
|
||||
if (avatarUrl != null && avatarUrl.isNotEmpty) {
|
||||
debugPrint('Found avatar URL: $avatarUrl');
|
||||
return avatarUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback ke data dari Supabase Auth
|
||||
final userMetadata = user.userMetadata;
|
||||
if (userMetadata != null && userMetadata.containsKey('avatar_url')) {
|
||||
return userMetadata['avatar_url']?.toString();
|
||||
}
|
||||
|
||||
return null; // No avatar found
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching user avatar: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk mendapatkan email pengguna
|
||||
Future<String?> getUserEmail() async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
debugPrint('No current user found when getting email');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Email ada di data user Supabase Auth
|
||||
return user.email;
|
||||
}
|
||||
|
||||
// Metode untuk mendapatkan NIK dari tabel warga_desa berdasarkan user_id
|
||||
Future<String?> getUserNIK() async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
debugPrint('No current user found when getting NIK');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('Fetching NIK for user_id: ${user.id}');
|
||||
|
||||
// Coba ambil NIK dari tabel warga_desa
|
||||
final userData =
|
||||
await client
|
||||
.from('warga_desa')
|
||||
.select('nik')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
// Jika berhasil mendapatkan data
|
||||
if (userData != null && userData.containsKey('nik')) {
|
||||
final nik = userData['nik']?.toString();
|
||||
if (nik != null && nik.isNotEmpty) {
|
||||
debugPrint('Found NIK: $nik');
|
||||
return nik;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback ke data dari metadata
|
||||
final userMetadata = user.userMetadata;
|
||||
if (userMetadata != null && userMetadata.containsKey('nik')) {
|
||||
return userMetadata['nik']?.toString();
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching user NIK: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk mendapatkan nomor telepon dari tabel warga_desa berdasarkan user_id
|
||||
Future<String?> getUserPhone() async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
debugPrint('No current user found when getting phone');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('Fetching phone for user_id: ${user.id}');
|
||||
|
||||
// Coba ambil nomor telepon dari tabel warga_desa
|
||||
final userData =
|
||||
await client
|
||||
.from('warga_desa')
|
||||
.select('nomor_telepon, no_telepon, phone')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
// Jika berhasil mendapatkan data, cek beberapa kemungkinan nama kolom
|
||||
if (userData != null) {
|
||||
if (userData.containsKey('nomor_telepon')) {
|
||||
final phone = userData['nomor_telepon']?.toString();
|
||||
if (phone != null && phone.isNotEmpty) return phone;
|
||||
}
|
||||
|
||||
if (userData.containsKey('no_telepon')) {
|
||||
final phone = userData['no_telepon']?.toString();
|
||||
if (phone != null && phone.isNotEmpty) return phone;
|
||||
}
|
||||
|
||||
if (userData.containsKey('phone')) {
|
||||
final phone = userData['phone']?.toString();
|
||||
if (phone != null && phone.isNotEmpty) return phone;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback ke data dari Supabase Auth
|
||||
final userMetadata = user.userMetadata;
|
||||
if (userMetadata != null) {
|
||||
if (userMetadata.containsKey('phone')) {
|
||||
return userMetadata['phone']?.toString();
|
||||
}
|
||||
if (userMetadata.containsKey('phone_number')) {
|
||||
return userMetadata['phone_number']?.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching user phone: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk mendapatkan alamat dari tabel warga_desa berdasarkan user_id
|
||||
Future<String?> getUserAddress() async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
debugPrint('No current user found when getting address');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('Fetching address for user_id: ${user.id}');
|
||||
|
||||
// Coba ambil alamat dari tabel warga_desa
|
||||
final userData =
|
||||
await client
|
||||
.from('warga_desa')
|
||||
.select('alamat')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
// Jika berhasil mendapatkan data
|
||||
if (userData != null && userData.containsKey('alamat')) {
|
||||
final address = userData['alamat']?.toString();
|
||||
if (address != null && address.isNotEmpty) {
|
||||
debugPrint('Found address: $address');
|
||||
return address;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback ke data dari Supabase Auth
|
||||
final userMetadata = user.userMetadata;
|
||||
if (userMetadata != null && userMetadata.containsKey('address')) {
|
||||
return userMetadata['address']?.toString();
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching user address: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Mendapatkan data sewa_aset berdasarkan status (misal: MENUNGGU PEMBAYARAN, PEMBAYARANAN DENDA)
|
||||
Future<List<Map<String, dynamic>>> getSewaAsetByStatus(
|
||||
List<String> statuses,
|
||||
) async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
debugPrint('No current user found when getting sewa_aset by status');
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
debugPrint(
|
||||
'Fetching sewa_aset for user_id: \\${user.id} with statuses: \\${statuses.join(', ')}',
|
||||
);
|
||||
// Supabase expects the IN filter as a comma-separated string in parentheses
|
||||
final statusString = '(${statuses.map((s) => '"$s"').join(',')})';
|
||||
final response = await client
|
||||
.from('sewa_aset')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.filter('status', 'in', statusString);
|
||||
debugPrint('Fetched sewa_aset count: \\${response.length}');
|
||||
// Pastikan response adalah List
|
||||
if (response is List) {
|
||||
return response
|
||||
.map<Map<String, dynamic>>(
|
||||
(item) => Map<String, dynamic>.from(item),
|
||||
)
|
||||
.toList();
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching sewa_aset by status: \\${e.toString()}');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
187
lib/app/data/providers/pesanan_provider.dart
Normal file
187
lib/app/data/providers/pesanan_provider.dart
Normal file
@ -0,0 +1,187 @@
|
||||
import 'package:bumrent_app/app/data/models/aset_model.dart';
|
||||
import 'package:bumrent_app/app/data/models/pesanan_model.dart';
|
||||
import 'package:bumrent_app/app/data/models/satuan_waktu_model.dart';
|
||||
import 'package:bumrent_app/app/data/providers/auth_provider.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class PesananProvider {
|
||||
final SupabaseClient _supabase = Supabase.instance.client;
|
||||
final _tableName = 'pesanan';
|
||||
|
||||
Future<List<PesananModel>> getPesananByUserId(String userId) async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
.from(_tableName)
|
||||
.select('*, aset(nama)')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', ascending: false);
|
||||
|
||||
final List<PesananModel> pesananList = [];
|
||||
for (final item in response) {
|
||||
final pesanan = PesananModel.fromJson(item);
|
||||
|
||||
// Attach the asset name
|
||||
if (item['aset'] != null) {
|
||||
pesanan.namaAset = item['aset']['nama'];
|
||||
}
|
||||
|
||||
// Get and attach satuan waktu name
|
||||
final satuanWaktu = await getSatuanWaktuById(pesanan.satuanWaktuId);
|
||||
if (satuanWaktu != null) {
|
||||
pesanan.namaSatuanWaktu = satuanWaktu.namaSatuanWaktu;
|
||||
}
|
||||
|
||||
pesananList.add(pesanan);
|
||||
}
|
||||
|
||||
return pesananList;
|
||||
} catch (e) {
|
||||
print('Error getting pesanan by user ID: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<PesananModel>> getAllPesanan() async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
.from(_tableName)
|
||||
.select('*, aset(nama), auth_users(full_name)')
|
||||
.order('created_at', ascending: false);
|
||||
|
||||
final List<PesananModel> pesananList = [];
|
||||
for (final item in response) {
|
||||
final pesanan = PesananModel.fromJson(item);
|
||||
|
||||
// Attach the asset name
|
||||
if (item['aset'] != null) {
|
||||
pesanan.namaAset = item['aset']['nama'];
|
||||
}
|
||||
|
||||
// Attach the user name
|
||||
if (item['auth_users'] != null) {
|
||||
pesanan.namaUser = item['auth_users']['full_name'];
|
||||
}
|
||||
|
||||
// Get and attach satuan waktu name
|
||||
final satuanWaktu = await getSatuanWaktuById(pesanan.satuanWaktuId);
|
||||
if (satuanWaktu != null) {
|
||||
pesanan.namaSatuanWaktu = satuanWaktu.namaSatuanWaktu;
|
||||
}
|
||||
|
||||
pesananList.add(pesanan);
|
||||
}
|
||||
|
||||
return pesananList;
|
||||
} catch (e) {
|
||||
print('Error getting all pesanan: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<PesananModel?> getPesananById(String id) async {
|
||||
try {
|
||||
final response =
|
||||
await _supabase
|
||||
.from(_tableName)
|
||||
.select('*, aset(nama), auth_users(full_name)')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
final pesanan = PesananModel.fromJson(response);
|
||||
|
||||
// Attach the asset name
|
||||
if (response['aset'] != null) {
|
||||
pesanan.namaAset = response['aset']['nama'];
|
||||
}
|
||||
|
||||
// Attach the user name
|
||||
if (response['auth_users'] != null) {
|
||||
pesanan.namaUser = response['auth_users']['full_name'];
|
||||
}
|
||||
|
||||
// Get and attach satuan waktu name
|
||||
final satuanWaktu = await getSatuanWaktuById(pesanan.satuanWaktuId);
|
||||
if (satuanWaktu != null) {
|
||||
pesanan.namaSatuanWaktu = satuanWaktu.namaSatuanWaktu;
|
||||
}
|
||||
|
||||
return pesanan;
|
||||
} catch (e) {
|
||||
print('Error getting pesanan by ID: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> createPesanan({
|
||||
required String asetId,
|
||||
required String satuanWaktuId,
|
||||
required String userId,
|
||||
required DateTime tanggalPemesanan,
|
||||
required String jamPemesanan,
|
||||
required int durasi,
|
||||
required int totalHarga,
|
||||
}) async {
|
||||
try {
|
||||
final response =
|
||||
await _supabase
|
||||
.from(_tableName)
|
||||
.insert({
|
||||
'aset_id': asetId,
|
||||
'satuan_waktu_id': satuanWaktuId,
|
||||
'user_id': userId,
|
||||
'status': 'pending',
|
||||
'tanggal_pemesanan':
|
||||
tanggalPemesanan.toIso8601String().split('T')[0],
|
||||
'jam_pemesanan': jamPemesanan,
|
||||
'durasi': durasi,
|
||||
'total_harga': totalHarga,
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
return response['id'];
|
||||
} catch (e) {
|
||||
print('Error creating pesanan: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> updatePesananStatus(String id, String status) async {
|
||||
try {
|
||||
await _supabase
|
||||
.from(_tableName)
|
||||
.update({
|
||||
'status': status,
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
})
|
||||
.eq('id', id);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('Error updating pesanan status: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deletePesanan(String id) async {
|
||||
try {
|
||||
await _supabase.from(_tableName).delete().eq('id', id);
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('Error deleting pesanan: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<SatuanWaktuModel?> getSatuanWaktuById(String id) async {
|
||||
try {
|
||||
final response =
|
||||
await _supabase.from('satuan_waktu').select().eq('id', id).single();
|
||||
return SatuanWaktuModel.fromJson(response);
|
||||
} catch (e) {
|
||||
print('Error getting satuan waktu by ID: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
lib/app/data/providers/supabase_provider.dart
Normal file
1
lib/app/data/providers/supabase_provider.dart
Normal file
@ -0,0 +1 @@
|
||||
|
||||
1
lib/app/data/repositories/rental_booking_repository.dart
Normal file
1
lib/app/data/repositories/rental_booking_repository.dart
Normal file
@ -0,0 +1 @@
|
||||
|
||||
1
lib/app/data/repositories/rental_item_repository.dart
Normal file
1
lib/app/data/repositories/rental_item_repository.dart
Normal file
@ -0,0 +1 @@
|
||||
|
||||
1
lib/app/data/repositories/user_repository.dart
Normal file
1
lib/app/data/repositories/user_repository.dart
Normal file
@ -0,0 +1 @@
|
||||
|
||||
242
lib/app/modules/auth/controllers/auth_controller.dart
Normal file
242
lib/app/modules/auth/controllers/auth_controller.dart
Normal file
@ -0,0 +1,242 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class AuthController extends GetxController {
|
||||
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
||||
|
||||
final emailController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
|
||||
// Form fields for registration
|
||||
final RxString email = ''.obs;
|
||||
final RxString password = ''.obs;
|
||||
final RxString nik = ''.obs;
|
||||
final RxString phoneNumber = ''.obs;
|
||||
final RxString selectedRole = 'WARGA'.obs; // Default role
|
||||
|
||||
// Form status
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxBool isPasswordVisible = false.obs;
|
||||
final RxString errorMessage = ''.obs;
|
||||
|
||||
// Role options
|
||||
final List<String> roleOptions = ['WARGA', 'PETUGAS_MITRA'];
|
||||
|
||||
void togglePasswordVisibility() {
|
||||
isPasswordVisible.value = !isPasswordVisible.value;
|
||||
}
|
||||
|
||||
// Change role selection
|
||||
void setRole(String? role) {
|
||||
if (role != null) {
|
||||
selectedRole.value = role;
|
||||
}
|
||||
}
|
||||
|
||||
void login() async {
|
||||
// Clear previous error messages
|
||||
errorMessage.value = '';
|
||||
|
||||
// Basic validation
|
||||
if (emailController.text.isEmpty || passwordController.text.isEmpty) {
|
||||
errorMessage.value = 'Email dan password tidak boleh kosong';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!GetUtils.isEmail(emailController.text.trim())) {
|
||||
errorMessage.value = 'Format email tidak valid';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// Use the actual Supabase authentication
|
||||
final response = await _authProvider.signIn(
|
||||
email: emailController.text.trim(),
|
||||
password: passwordController.text,
|
||||
);
|
||||
|
||||
// Check if login was successful
|
||||
if (response.user != null) {
|
||||
await _checkRoleAndNavigate();
|
||||
} else {
|
||||
errorMessage.value = 'Login gagal. Periksa email dan password Anda.';
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Terjadi kesalahan: ${e.toString()}';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkRoleAndNavigate() async {
|
||||
try {
|
||||
// Get the user's role ID from the auth provider
|
||||
final roleId = await _authProvider.getUserRoleId();
|
||||
|
||||
if (roleId == null) {
|
||||
errorMessage.value = 'Tidak dapat memperoleh peran pengguna';
|
||||
return;
|
||||
}
|
||||
|
||||
// Get role name based on role ID
|
||||
final roleName = await _authProvider.getRoleName(roleId);
|
||||
|
||||
// Navigate based on role name
|
||||
if (roleName == null) {
|
||||
_navigateToWargaDashboard(); // Default to warga if role name not found
|
||||
return;
|
||||
}
|
||||
|
||||
switch (roleName.toUpperCase()) {
|
||||
case 'PETUGAS_BUMDES':
|
||||
_navigateToPetugasBumdesDashboard();
|
||||
break;
|
||||
case 'WARGA':
|
||||
default:
|
||||
_navigateToWargaDashboard();
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Gagal navigasi: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToPetugasBumdesDashboard() {
|
||||
Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
|
||||
}
|
||||
|
||||
void _navigateToWargaDashboard() {
|
||||
Get.offAllNamed(Routes.WARGA_DASHBOARD);
|
||||
}
|
||||
|
||||
void forgotPassword() async {
|
||||
// Clear previous error messages
|
||||
errorMessage.value = '';
|
||||
|
||||
// Basic validation
|
||||
if (emailController.text.isEmpty) {
|
||||
errorMessage.value = 'Email tidak boleh kosong';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!GetUtils.isEmail(emailController.text.trim())) {
|
||||
errorMessage.value = 'Format email tidak valid';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// Call Supabase to send password reset email
|
||||
await _authProvider.client.auth.resetPasswordForEmail(
|
||||
emailController.text.trim(),
|
||||
);
|
||||
|
||||
// Show success message
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Link reset password telah dikirim ke email Anda',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green[100],
|
||||
colorText: Colors.green[800],
|
||||
icon: const Icon(Icons.check_circle, color: Colors.green),
|
||||
);
|
||||
|
||||
// Return to login page after a short delay
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
Get.back();
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Terjadi kesalahan: ${e.toString()}';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void goToSignUp() {
|
||||
// Clear error message when navigating away
|
||||
errorMessage.value = '';
|
||||
Get.toNamed(Routes.REGISTER);
|
||||
}
|
||||
|
||||
void goToForgotPassword() {
|
||||
// Clear error message when navigating away
|
||||
errorMessage.value = '';
|
||||
Get.toNamed(Routes.FORGOT_PASSWORD);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
emailController.dispose();
|
||||
passwordController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Register user implementation
|
||||
Future<void> registerUser() async {
|
||||
// Validate all required fields
|
||||
if (email.value.isEmpty ||
|
||||
password.value.isEmpty ||
|
||||
nik.value.isEmpty ||
|
||||
phoneNumber.value.isEmpty) {
|
||||
errorMessage.value = 'Semua field harus diisi';
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation for email
|
||||
if (!GetUtils.isEmail(email.value.trim())) {
|
||||
errorMessage.value = 'Format email tidak valid';
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation for password
|
||||
if (password.value.length < 6) {
|
||||
errorMessage.value = 'Password minimal 6 karakter';
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation for NIK
|
||||
if (nik.value.length != 16) {
|
||||
errorMessage.value = 'NIK harus 16 digit';
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation for phone number
|
||||
if (!phoneNumber.value.startsWith('08') || phoneNumber.value.length < 10) {
|
||||
errorMessage.value =
|
||||
'Nomor HP tidak valid (harus diawali 08 dan minimal 10 digit)';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
// Create user with Supabase
|
||||
final response = await _authProvider.signUp(
|
||||
email: email.value.trim(),
|
||||
password: password.value,
|
||||
data: {
|
||||
'nik': nik.value.trim(),
|
||||
'phone_number': phoneNumber.value.trim(),
|
||||
'role': selectedRole.value,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.user != null) {
|
||||
// Registration successful
|
||||
Get.offNamed(Routes.REGISTRATION_SUCCESS);
|
||||
} else {
|
||||
errorMessage.value = 'Gagal mendaftar. Silakan coba lagi.';
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Terjadi kesalahan: ${e.toString()}';
|
||||
print('Registration error: ${e.toString()}');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
376
lib/app/modules/auth/views/forgot_password_view.dart
Normal file
376
lib/app/modules/auth/views/forgot_password_view.dart
Normal file
@ -0,0 +1,376 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/auth_controller.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
|
||||
class ForgotPasswordView extends GetView<AuthController> {
|
||||
const ForgotPasswordView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
// Background gradient
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [AppColors.primarySoft, AppColors.background],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Background pattern
|
||||
Opacity(
|
||||
opacity: 0.03,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/pattern.png'),
|
||||
repeat: ImageRepeat.repeat,
|
||||
scale: 4.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Accent circle
|
||||
Positioned(
|
||||
top: -100,
|
||||
right: -80,
|
||||
child: Container(
|
||||
width: 220,
|
||||
height: 220,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
AppColors.primary.withOpacity(0.2),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Main content
|
||||
SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Back button
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||
color: AppColors.primary,
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
|
||||
// Scrollable content
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 40),
|
||||
_buildEmailField(),
|
||||
const SizedBox(height: 32),
|
||||
_buildResetButton(),
|
||||
const SizedBox(height: 40),
|
||||
_buildImportantInfo(),
|
||||
const SizedBox(height: 24),
|
||||
_buildBackToLoginLink(),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Column(
|
||||
children: [
|
||||
// Floating lock icon with animation effect
|
||||
Container(
|
||||
width: 110,
|
||||
height: 110,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primary.withOpacity(0.2),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 5,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.lock_open_rounded,
|
||||
size: 50,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Lupa Password?',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Masukkan email Anda di bawah ini dan kami akan mengirimkan link untuk reset password.',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: AppColors.textSecondary,
|
||||
height: 1.5,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmailField() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0, bottom: 8.0),
|
||||
child: Text(
|
||||
'Email',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller.emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Masukkan email Anda',
|
||||
hintStyle: TextStyle(color: AppColors.textLight),
|
||||
prefixIcon: Icon(
|
||||
Icons.email_outlined,
|
||||
color: AppColors.iconGrey,
|
||||
size: 22,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColors.surface,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Error message
|
||||
Obx(
|
||||
() =>
|
||||
controller.errorMessage.value.isNotEmpty
|
||||
? Container(
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResetButton() {
|
||||
return Obx(
|
||||
() => SizedBox(
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
controller.isLoading.value ? null : controller.forgotPassword,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 2,
|
||||
shadowColor: AppColors.primary.withOpacity(0.4),
|
||||
disabledBackgroundColor: AppColors.primary.withOpacity(0.6),
|
||||
),
|
||||
child:
|
||||
controller.isLoading.value
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Kirim Link Reset',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.send_rounded, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImportantInfo() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.green.shade100),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade100,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.info_outline,
|
||||
size: 20,
|
||||
color: Colors.green.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Informasi Penting',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Petunjuk reset password akan dikirim ke email Anda. Silakan periksa kotak masuk atau folder spam setelah permintaan reset password.',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.green.shade900,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBackToLoginLink() {
|
||||
return Center(
|
||||
child: TextButton.icon(
|
||||
onPressed: () => Get.back(),
|
||||
icon: Icon(
|
||||
Icons.arrow_back_rounded,
|
||||
size: 16,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
label: Text(
|
||||
'Kembali ke Login',
|
||||
style: TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
368
lib/app/modules/auth/views/login_view.dart
Normal file
368
lib/app/modules/auth/views/login_view.dart
Normal file
@ -0,0 +1,368 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/auth_controller.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
|
||||
class LoginView extends GetView<AuthController> {
|
||||
const LoginView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
// Background gradient
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topRight,
|
||||
end: Alignment.bottomLeft,
|
||||
colors: [
|
||||
AppColors.primaryLight.withOpacity(0.1),
|
||||
AppColors.background,
|
||||
AppColors.accentLight.withOpacity(0.1),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Pattern overlay
|
||||
Opacity(
|
||||
opacity: 0.03,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/pattern.png'),
|
||||
repeat: ImageRepeat.repeat,
|
||||
scale: 4.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Accent circles
|
||||
Positioned(
|
||||
top: -40,
|
||||
right: -20,
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 150,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
AppColors.primary.withOpacity(0.2),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -50,
|
||||
left: -30,
|
||||
child: Container(
|
||||
width: 180,
|
||||
height: 180,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
AppColors.accent.withOpacity(0.2),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Main content
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 50),
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 40),
|
||||
_buildLoginCard(),
|
||||
const SizedBox(height: 24),
|
||||
_buildRegisterLink(),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Center(
|
||||
child: Hero(
|
||||
tag: 'logo',
|
||||
child: Image.asset(
|
||||
'assets/images/logo.png',
|
||||
width: 220,
|
||||
height: 220,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Icon(
|
||||
Icons.apartment_rounded,
|
||||
size: 180,
|
||||
color: AppColors.primary,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoginCard() {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shadowColor: AppColors.shadow,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(28.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Welcome text
|
||||
Text(
|
||||
'Selamat Datang',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Masuk untuk melanjutkan ke akun Anda',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Email field
|
||||
_buildInputLabel('Email'),
|
||||
const SizedBox(height: 8),
|
||||
_buildTextField(
|
||||
controller: controller.emailController,
|
||||
hintText: 'Masukkan email Anda',
|
||||
prefixIcon: Icons.email_outlined,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Password field
|
||||
_buildInputLabel('Password'),
|
||||
const SizedBox(height: 8),
|
||||
Obx(
|
||||
() => _buildTextField(
|
||||
controller: controller.passwordController,
|
||||
hintText: 'Masukkan password Anda',
|
||||
prefixIcon: Icons.lock_outline,
|
||||
obscureText: !controller.isPasswordVisible.value,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
controller.isPasswordVisible.value
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: AppColors.iconGrey,
|
||||
),
|
||||
onPressed: controller.togglePasswordVisibility,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Forgot password
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () => controller.goToForgotPassword(),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 0,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Lupa sandi?',
|
||||
style: TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Login button
|
||||
Obx(
|
||||
() => SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
controller.isLoading.value ? null : controller.login,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: AppColors.buttonText,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: controller.isLoading.value ? 0 : 2,
|
||||
shadowColor: AppColors.primary.withOpacity(0.4),
|
||||
),
|
||||
child:
|
||||
controller.isLoading.value
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Masuk',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.arrow_forward, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Error message
|
||||
Obx(
|
||||
() =>
|
||||
controller.errorMessage.value.isNotEmpty
|
||||
? Container(
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputLabel(String label) {
|
||||
return Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 15,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField({
|
||||
required TextEditingController controller,
|
||||
required String hintText,
|
||||
required IconData prefixIcon,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
bool obscureText = false,
|
||||
Widget? suffixIcon,
|
||||
}) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
obscureText: obscureText,
|
||||
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: TextStyle(color: AppColors.textLight),
|
||||
prefixIcon: Icon(prefixIcon, color: AppColors.iconGrey, size: 22),
|
||||
suffixIcon: suffixIcon,
|
||||
filled: true,
|
||||
fillColor: AppColors.inputBackground,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRegisterLink() {
|
||||
return Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Belum punya akun?",
|
||||
style: TextStyle(color: AppColors.textSecondary),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: controller.goToSignUp,
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
child: Text(
|
||||
'Daftar',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
266
lib/app/modules/auth/views/registration_success_view.dart
Normal file
266
lib/app/modules/auth/views/registration_success_view.dart
Normal file
@ -0,0 +1,266 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
|
||||
class RegistrationSuccessView extends StatefulWidget {
|
||||
const RegistrationSuccessView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<RegistrationSuccessView> createState() =>
|
||||
_RegistrationSuccessViewState();
|
||||
}
|
||||
|
||||
class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.elasticOut),
|
||||
);
|
||||
|
||||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.4, 1.0, curve: Curves.easeInOut),
|
||||
),
|
||||
);
|
||||
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Background elements
|
||||
Positioned(
|
||||
top: -120,
|
||||
left: -120,
|
||||
child: Container(
|
||||
width: 300,
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.successLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: -80,
|
||||
bottom: 100,
|
||||
child: Container(
|
||||
width: 200,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.primaryLight.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Confetti particles
|
||||
Positioned.fill(child: _buildConfettiParticles()),
|
||||
|
||||
// Main content
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildSuccessAnimation(),
|
||||
const SizedBox(height: 40),
|
||||
_buildSuccessMessage(),
|
||||
const SizedBox(height: 40),
|
||||
_buildBackToLoginButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConfettiParticles() {
|
||||
return Stack(
|
||||
children: List.generate(20, (index) {
|
||||
final left = (index * 20) % MediaQuery.of(context).size.width;
|
||||
final top = (index * 30) % MediaQuery.of(context).size.height;
|
||||
final size = 8.0 + (index % 5) * 2;
|
||||
|
||||
final colors = [
|
||||
AppColors.success,
|
||||
AppColors.primary,
|
||||
AppColors.accent,
|
||||
AppColors.primaryLight,
|
||||
];
|
||||
|
||||
return Positioned(
|
||||
left: left.toDouble(),
|
||||
top: top.toDouble(),
|
||||
child: AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
final delay = index * 0.1;
|
||||
final startTime = delay;
|
||||
final endTime = startTime + 0.8;
|
||||
|
||||
double opacity = 0.0;
|
||||
if (_animationController.value >= startTime) {
|
||||
opacity =
|
||||
(_animationController.value - startTime) /
|
||||
(endTime - startTime);
|
||||
if (opacity > 1.0) opacity = 1.0;
|
||||
}
|
||||
|
||||
return Opacity(
|
||||
opacity: opacity,
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: colors[index % colors.length],
|
||||
shape:
|
||||
index % 2 == 0 ? BoxShape.circle : BoxShape.rectangle,
|
||||
borderRadius:
|
||||
index % 2 == 0 ? null : BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuccessAnimation() {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Hero(
|
||||
tag: 'success',
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.success,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.success.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.check,
|
||||
size: 70,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuccessMessage() {
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _fadeAnimation.value,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Pendaftaran Berhasil!',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
'Akun Anda telah berhasil terdaftar. Silakan masuk dengan email dan password yang telah Anda daftarkan.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.textSecondary,
|
||||
height: 1.5,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBackToLoginButton() {
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _fadeAnimation.value,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// Navigate back to login page
|
||||
Get.offAllNamed('/login');
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: AppColors.buttonText,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text(
|
||||
'Masuk Sekarang',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
549
lib/app/modules/auth/views/registration_view.dart
Normal file
549
lib/app/modules/auth/views/registration_view.dart
Normal file
@ -0,0 +1,549 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/auth_controller.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
|
||||
class RegistrationView extends GetView<AuthController> {
|
||||
const RegistrationView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Background gradient
|
||||
Positioned(
|
||||
top: -100,
|
||||
right: -100,
|
||||
child: Container(
|
||||
width: 300,
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
AppColors.primaryLight.withOpacity(0.2),
|
||||
AppColors.background.withOpacity(0),
|
||||
],
|
||||
stops: const [0.0, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -80,
|
||||
left: -80,
|
||||
child: Container(
|
||||
width: 200,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
AppColors.accent.withOpacity(0.15),
|
||||
AppColors.background.withOpacity(0),
|
||||
],
|
||||
stops: const [0.0, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Content
|
||||
SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildBackButton(),
|
||||
const SizedBox(height: 20),
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 24),
|
||||
_buildRegistrationForm(),
|
||||
const SizedBox(height: 32),
|
||||
_buildRegisterButton(),
|
||||
const SizedBox(height: 24),
|
||||
_buildImportantInfo(),
|
||||
const SizedBox(height: 24),
|
||||
_buildLoginLink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBackButton() {
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: InkWell(
|
||||
onTap: () => Get.back(),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.arrow_back,
|
||||
size: 20,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Hero(
|
||||
tag: 'logo',
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primarySoft,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primary.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.apartment_rounded,
|
||||
size: 40,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Daftar Akun',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Lengkapi data berikut untuk mendaftar',
|
||||
style: TextStyle(fontSize: 16, color: AppColors.textSecondary),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImportantInfo() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.warningLight,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.warning.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.warning.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.info_outline, size: 20, color: AppColors.warning),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Informasi Penting',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.warning,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Pendaftaran hanya dapat dilakukan oleh warga dan mitra yang sudah terverivikasi. Silahkan hubungi petugas atau kunjungi kantor untuk informasi lebih lanjut.',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.textPrimary,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRegistrationForm() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Email Input
|
||||
_buildInputLabel('Email'),
|
||||
const SizedBox(height: 8),
|
||||
_buildEmailField(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Password Input
|
||||
_buildInputLabel('Password'),
|
||||
const SizedBox(height: 8),
|
||||
_buildPasswordField(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// NIK Input
|
||||
_buildInputLabel('NIK'),
|
||||
const SizedBox(height: 8),
|
||||
_buildNikField(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Phone Number Input
|
||||
_buildInputLabel('No. Hp'),
|
||||
const SizedBox(height: 8),
|
||||
_buildPhoneField(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Role Selection Dropdown
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Daftar Sebagai',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[300]!, width: 1),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Obx(
|
||||
() => DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
isExpanded: true,
|
||||
value: controller.selectedRole.value,
|
||||
hint: const Text('Pilih Peran'),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'WARGA',
|
||||
child: const Text('Warga'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'PETUGAS_MITRA',
|
||||
child: const Text('Mitra'),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
controller.setRole(value);
|
||||
},
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
style: const TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Error message
|
||||
Obx(
|
||||
() =>
|
||||
controller.errorMessage.value.isNotEmpty
|
||||
? Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.errorLight,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
color: AppColors.error,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
controller.errorMessage.value,
|
||||
style: TextStyle(
|
||||
color: AppColors.error,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputLabel(String label) {
|
||||
return Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmailField() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
onChanged: (value) => controller.email.value = value,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Masukkan email anda',
|
||||
hintStyle: TextStyle(color: AppColors.textLight),
|
||||
prefixIcon: Icon(Icons.email_outlined, color: AppColors.primary),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPasswordField() {
|
||||
return Obx(
|
||||
() => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
onChanged: (value) => controller.password.value = value,
|
||||
obscureText: !controller.isPasswordVisible.value,
|
||||
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Masukkan password anda',
|
||||
hintStyle: TextStyle(color: AppColors.textLight),
|
||||
prefixIcon: Icon(Icons.lock_outlined, color: AppColors.primary),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
controller.isPasswordVisible.value
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: AppColors.iconGrey,
|
||||
),
|
||||
onPressed: controller.togglePasswordVisibility,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNikField() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
onChanged: (value) => controller.nik.value = value,
|
||||
keyboardType: TextInputType.number,
|
||||
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Masukkan NIK anda',
|
||||
hintStyle: TextStyle(color: AppColors.textLight),
|
||||
prefixIcon: Icon(
|
||||
Icons.credit_card_outlined,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPhoneField() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
onChanged: (value) => controller.phoneNumber.value = value,
|
||||
keyboardType: TextInputType.phone,
|
||||
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Masukkan nomor HP anda',
|
||||
hintStyle: TextStyle(color: AppColors.textLight),
|
||||
prefixIcon: Icon(Icons.phone_outlined, color: AppColors.primary),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRegisterButton() {
|
||||
return Obx(
|
||||
() => ElevatedButton(
|
||||
onPressed: controller.isLoading.value ? null : controller.registerUser,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: AppColors.buttonText,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 0,
|
||||
disabledBackgroundColor: AppColors.primary.withOpacity(0.6),
|
||||
),
|
||||
child:
|
||||
controller.isLoading.value
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Daftar',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoginLink() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Sudah punya akun? ',
|
||||
style: TextStyle(color: AppColors.textSecondary, fontSize: 14),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Get.back(); // Back to login page
|
||||
},
|
||||
child: Text(
|
||||
'Masuk',
|
||||
style: TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/list_pelanggan_aktif_controller.dart';
|
||||
|
||||
class ListPelangganAktifBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<ListPelangganAktifController>(
|
||||
() => ListPelangganAktifController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/list_petugas_mitra_controller.dart';
|
||||
|
||||
class ListPetugasMitraBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<ListPetugasMitraController>(() => ListPetugasMitraController());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/list_tagihan_periode_controller.dart';
|
||||
|
||||
class ListTagihanPeriodeBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<ListTagihanPeriodeController>(
|
||||
() => ListTagihanPeriodeController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_aset_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
|
||||
class PetugasAsetBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Ensure dashboard controller is registered
|
||||
if (!Get.isRegistered<PetugasBumdesDashboardController>()) {
|
||||
Get.put(PetugasBumdesDashboardController(), permanent: true);
|
||||
}
|
||||
|
||||
Get.lazyPut<PetugasAsetController>(() => PetugasAsetController());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_bumdes_cbp_controller.dart';
|
||||
|
||||
class PetugasBumdesCbpBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<PetugasBumdesCbpController>(() => PetugasBumdesCbpController());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_sewa_controller.dart';
|
||||
|
||||
class PetugasDetailSewaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Memastikan controller sudah tersedia
|
||||
Get.lazyPut<PetugasSewaController>(
|
||||
() => PetugasSewaController(),
|
||||
fenix: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_manajemen_bumdes_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
|
||||
class PetugasManajemenBumdesBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Make sure AuthProvider is registered
|
||||
if (!Get.isRegistered<AuthProvider>()) {
|
||||
Get.put(AuthProvider());
|
||||
}
|
||||
|
||||
// Register the dashboard controller if not already registered
|
||||
if (!Get.isRegistered<PetugasBumdesDashboardController>()) {
|
||||
Get.put<PetugasBumdesDashboardController>(
|
||||
PetugasBumdesDashboardController(),
|
||||
permanent: true,
|
||||
);
|
||||
}
|
||||
|
||||
// Register the manajemen bumdes controller
|
||||
Get.lazyPut<PetugasManajemenBumdesController>(
|
||||
() => PetugasManajemenBumdesController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_paket_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
|
||||
class PetugasPaketBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Ensure dashboard controller is registered
|
||||
if (!Get.isRegistered<PetugasBumdesDashboardController>()) {
|
||||
Get.put(PetugasBumdesDashboardController(), permanent: true);
|
||||
}
|
||||
|
||||
Get.lazyPut<PetugasPaketController>(() => PetugasPaketController());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_sewa_controller.dart';
|
||||
|
||||
class PetugasSewaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<PetugasSewaController>(() => PetugasSewaController());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_tambah_aset_controller.dart';
|
||||
|
||||
class PetugasTambahAsetBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<PetugasTambahAsetController>(
|
||||
() => PetugasTambahAsetController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_tambah_paket_controller.dart';
|
||||
|
||||
class PetugasTambahPaketBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<PetugasTambahPaketController>(
|
||||
() => PetugasTambahPaketController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ListPelangganAktifController extends GetxController {
|
||||
// Reactive variables
|
||||
final isLoading = true.obs;
|
||||
final pelangganList = <Map<String, dynamic>>[].obs;
|
||||
final searchQuery = ''.obs;
|
||||
final serviceName = ''.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// Get the service name passed from previous page
|
||||
if (Get.arguments != null && Get.arguments['serviceName'] != null) {
|
||||
serviceName.value = Get.arguments['serviceName'];
|
||||
}
|
||||
|
||||
// Load the pelanggan data
|
||||
loadPelangganData();
|
||||
}
|
||||
|
||||
// Load sample pelanggan data (would be replaced with API call in production)
|
||||
Future<void> loadPelangganData() async {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// Simulate API call delay
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
|
||||
// For now, we only have Malih as an active subscriber
|
||||
final sampleData = [
|
||||
{
|
||||
'id': '1',
|
||||
'nama': 'Malih',
|
||||
'alamat': 'Jl. Desa Sejahtera No. 15, RT 03/RW 02',
|
||||
'status': 'Aktif',
|
||||
'tanggal_mulai': '01/05/2023',
|
||||
'tanggal_berakhir': '01/05/2024',
|
||||
'pembayaran_terakhir': '01/04/2024',
|
||||
'tagihan': 'Rp 20.000',
|
||||
'telepon': '081234567890',
|
||||
'email': 'malih@example.com',
|
||||
'catatan': 'Pelanggan setia sejak 2023',
|
||||
},
|
||||
];
|
||||
|
||||
pelangganList.assignAll(sampleData);
|
||||
} catch (e) {
|
||||
print('Error loading pelanggan data: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter the list based on search query
|
||||
List<Map<String, dynamic>> get filteredPelangganList {
|
||||
if (searchQuery.value.isEmpty) {
|
||||
return pelangganList;
|
||||
}
|
||||
|
||||
final query = searchQuery.value.toLowerCase();
|
||||
return pelangganList.where((pelanggan) {
|
||||
final nama = pelanggan['nama'].toString().toLowerCase();
|
||||
final alamat = pelanggan['alamat'].toString().toLowerCase();
|
||||
final status = pelanggan['status'].toString().toLowerCase();
|
||||
|
||||
return nama.contains(query) ||
|
||||
alamat.contains(query) ||
|
||||
status.contains(query);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Update search query
|
||||
void updateSearchQuery(String query) {
|
||||
searchQuery.value = query;
|
||||
}
|
||||
|
||||
// Get status color based on status value
|
||||
getStatusColor(String status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'aktif':
|
||||
return 0xFF4CAF50; // Green
|
||||
case 'tertunda':
|
||||
return 0xFFFFA000; // Amber
|
||||
case 'berakhir':
|
||||
return 0xFF9E9E9E; // Grey
|
||||
case 'dibatalkan':
|
||||
return 0xFFE53935; // Red
|
||||
default:
|
||||
return 0xFF2196F3; // Blue
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ListPetugasMitraController extends GetxController {
|
||||
// Observable list of partners/mitra
|
||||
final partners =
|
||||
<Map<String, dynamic>>[
|
||||
{
|
||||
'id': '1',
|
||||
'name': 'Malih',
|
||||
'contact': '081234567890',
|
||||
'address': 'Jl. Desa No. 123, Kecamatan Bumdes, Kabupaten Desa',
|
||||
'is_active': true,
|
||||
'role': 'Petugas Lapangan',
|
||||
'join_date': '10 Januari 2023',
|
||||
},
|
||||
].obs;
|
||||
|
||||
// Loading state
|
||||
final isLoading = false.obs;
|
||||
|
||||
// Search functionality
|
||||
final searchQuery = ''.obs;
|
||||
|
||||
// Filtered list based on search
|
||||
List<Map<String, dynamic>> get filteredPartners {
|
||||
if (searchQuery.value.isEmpty) {
|
||||
return partners;
|
||||
}
|
||||
return partners
|
||||
.where(
|
||||
(partner) =>
|
||||
partner['name'].toString().toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
) ||
|
||||
partner['contact'].toString().toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
) ||
|
||||
partner['role'].toString().toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Add a new partner
|
||||
void addPartner(Map<String, dynamic> partner) {
|
||||
partners.add(partner);
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Petugas mitra berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
// Edit an existing partner
|
||||
void editPartner(String id, Map<String, dynamic> updatedPartner) {
|
||||
final index = partners.indexWhere((partner) => partner['id'] == id);
|
||||
if (index != -1) {
|
||||
partners[index] = updatedPartner;
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Data petugas mitra berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete a partner
|
||||
void deletePartner(String id) {
|
||||
partners.removeWhere((partner) => partner['id'] == id);
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Petugas mitra berhasil dihapus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
// Toggle partner active status
|
||||
void togglePartnerStatus(String id) {
|
||||
final index = partners.indexWhere((partner) => partner['id'] == id);
|
||||
if (index != -1) {
|
||||
final currentStatus = partners[index]['is_active'] as bool;
|
||||
partners[index]['is_active'] = !currentStatus;
|
||||
Get.snackbar(
|
||||
'Status Diperbarui',
|
||||
'Status petugas mitra diubah menjadi ${!currentStatus ? 'Aktif' : 'Nonaktif'}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,106 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ListTagihanPeriodeController extends GetxController {
|
||||
// Reactive variables
|
||||
final isLoading = true.obs;
|
||||
final periodeList = <Map<String, dynamic>>[].obs;
|
||||
final searchQuery = ''.obs;
|
||||
|
||||
// Customer data
|
||||
final pelangganData = Rx<Map<String, dynamic>>({});
|
||||
final serviceName = ''.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// Get the customer data and service name passed from previous page
|
||||
if (Get.arguments != null) {
|
||||
if (Get.arguments['pelanggan'] != null) {
|
||||
pelangganData.value = Map<String, dynamic>.from(
|
||||
Get.arguments['pelanggan'],
|
||||
);
|
||||
}
|
||||
|
||||
if (Get.arguments['serviceName'] != null) {
|
||||
serviceName.value = Get.arguments['serviceName'];
|
||||
}
|
||||
}
|
||||
|
||||
// Load periode data
|
||||
loadPeriodeData();
|
||||
}
|
||||
|
||||
// Load sample periode data (would be replaced with API call in production)
|
||||
Future<void> loadPeriodeData() async {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// Simulate API call delay
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
|
||||
// Sample data for periods
|
||||
final sampleData = [
|
||||
{
|
||||
'id': '1',
|
||||
'bulan': 'Maret',
|
||||
'tahun': '2025',
|
||||
'nominal': 'Rp 20.000',
|
||||
'status_pembayaran': 'Lunas',
|
||||
'tanggal_pembayaran': '05/03/2025',
|
||||
'metode_pembayaran': 'Transfer Bank',
|
||||
'keterangan': 'Pembayaran tepat waktu',
|
||||
'is_current': true,
|
||||
},
|
||||
];
|
||||
|
||||
periodeList.assignAll(sampleData);
|
||||
} catch (e) {
|
||||
print('Error loading periode data: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter the list based on search query
|
||||
List<Map<String, dynamic>> get filteredPeriodeList {
|
||||
if (searchQuery.value.isEmpty) {
|
||||
return periodeList;
|
||||
}
|
||||
|
||||
final query = searchQuery.value.toLowerCase();
|
||||
return periodeList.where((periode) {
|
||||
final bulan = periode['bulan'].toString().toLowerCase();
|
||||
final tahun = periode['tahun'].toString().toLowerCase();
|
||||
final status = periode['status_pembayaran'].toString().toLowerCase();
|
||||
|
||||
return bulan.contains(query) ||
|
||||
tahun.contains(query) ||
|
||||
status.contains(query);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Update search query
|
||||
void updateSearchQuery(String query) {
|
||||
searchQuery.value = query;
|
||||
}
|
||||
|
||||
// Get status color based on payment status
|
||||
getStatusColor(String status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'lunas':
|
||||
return 0xFF4CAF50; // Green
|
||||
case 'belum lunas':
|
||||
return 0xFFFFA000; // Amber
|
||||
case 'terlambat':
|
||||
return 0xFFE53935; // Red
|
||||
default:
|
||||
return 0xFF2196F3; // Blue
|
||||
}
|
||||
}
|
||||
|
||||
// Get formatted month-year string
|
||||
String getPeriodeString(Map<String, dynamic> periode) {
|
||||
return '${periode['bulan']} ${periode['tahun']}';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,217 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PetugasAsetController extends GetxController {
|
||||
// Observable lists for asset data
|
||||
final asetList = <Map<String, dynamic>>[].obs;
|
||||
final filteredAsetList = <Map<String, dynamic>>[].obs;
|
||||
final isLoading = true.obs;
|
||||
final searchQuery = ''.obs;
|
||||
|
||||
// Tab selection (0 for Sewa, 1 for Langganan)
|
||||
final selectedTabIndex = 0.obs;
|
||||
|
||||
// Sort options
|
||||
final sortBy = 'Nama (A-Z)'.obs;
|
||||
final sortOptions =
|
||||
[
|
||||
'Nama (A-Z)',
|
||||
'Nama (Z-A)',
|
||||
'Harga (Rendah-Tinggi)',
|
||||
'Harga (Tinggi-Rendah)',
|
||||
].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// Load sample data when the controller is initialized
|
||||
loadAsetData();
|
||||
}
|
||||
|
||||
// Load sample asset data (would be replaced with API call in production)
|
||||
Future<void> loadAsetData() async {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// Simulate API call with a delay
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
// Sample assets data
|
||||
final sampleData = [
|
||||
{
|
||||
'id': '1',
|
||||
'nama': 'Meja Rapat',
|
||||
'kategori': 'Furniture',
|
||||
'jenis': 'Sewa', // Added jenis field
|
||||
'harga': 50000,
|
||||
'satuan': 'per hari',
|
||||
'stok': 10,
|
||||
'deskripsi':
|
||||
'Meja rapat kayu jati ukuran besar untuk acara pertemuan',
|
||||
'gambar': 'https://example.com/meja.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
{
|
||||
'id': '2',
|
||||
'nama': 'Kursi Taman',
|
||||
'kategori': 'Furniture',
|
||||
'jenis': 'Sewa', // Added jenis field
|
||||
'harga': 10000,
|
||||
'satuan': 'per hari',
|
||||
'stok': 50,
|
||||
'deskripsi': 'Kursi taman plastik yang nyaman untuk acara outdoor',
|
||||
'gambar': 'https://example.com/kursi.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
{
|
||||
'id': '3',
|
||||
'nama': 'Proyektor',
|
||||
'kategori': 'Elektronik',
|
||||
'jenis': 'Sewa', // Added jenis field
|
||||
'harga': 100000,
|
||||
'satuan': 'per hari',
|
||||
'stok': 5,
|
||||
'deskripsi': 'Proyektor HD dengan brightness tinggi',
|
||||
'gambar': 'https://example.com/proyektor.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
{
|
||||
'id': '4',
|
||||
'nama': 'Sound System',
|
||||
'kategori': 'Elektronik',
|
||||
'jenis': 'Langganan', // Added jenis field
|
||||
'harga': 200000,
|
||||
'satuan': 'per bulan',
|
||||
'stok': 3,
|
||||
'deskripsi': 'Sound system lengkap dengan speaker dan mixer',
|
||||
'gambar': 'https://example.com/sound.jpg',
|
||||
'tersedia': false,
|
||||
},
|
||||
{
|
||||
'id': '5',
|
||||
'nama': 'Mobil Pick Up',
|
||||
'kategori': 'Kendaraan',
|
||||
'jenis': 'Langganan', // Added jenis field
|
||||
'harga': 250000,
|
||||
'satuan': 'per bulan',
|
||||
'stok': 2,
|
||||
'deskripsi': 'Mobil pick up untuk mengangkut barang',
|
||||
'gambar': 'https://example.com/pickup.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
{
|
||||
'id': '6',
|
||||
'nama': 'Internet Fiber',
|
||||
'kategori': 'Elektronik',
|
||||
'jenis': 'Langganan', // Added jenis field
|
||||
'harga': 350000,
|
||||
'satuan': 'per bulan',
|
||||
'stok': 15,
|
||||
'deskripsi': 'Paket internet fiber 100Mbps untuk kantor',
|
||||
'gambar': 'https://example.com/internet.jpg',
|
||||
'tersedia': true,
|
||||
},
|
||||
];
|
||||
|
||||
asetList.assignAll(sampleData);
|
||||
applyFilters(); // Apply default filters
|
||||
} catch (e) {
|
||||
print('Error loading asset data: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filters and sorting to asset list
|
||||
void applyFilters() {
|
||||
// Start with all assets
|
||||
var filtered = List<Map<String, dynamic>>.from(asetList);
|
||||
|
||||
// Filter by tab selection (Sewa or Langganan)
|
||||
String jenisFilter = selectedTabIndex.value == 0 ? 'Sewa' : 'Langganan';
|
||||
filtered = filtered.where((aset) => aset['jenis'] == jenisFilter).toList();
|
||||
|
||||
// Apply search query
|
||||
if (searchQuery.value.isNotEmpty) {
|
||||
final query = searchQuery.value.toLowerCase();
|
||||
filtered =
|
||||
filtered.where((aset) {
|
||||
final nama = aset['nama'].toString().toLowerCase();
|
||||
final deskripsi = aset['deskripsi'].toString().toLowerCase();
|
||||
final kategori = aset['kategori'].toString().toLowerCase();
|
||||
|
||||
return nama.contains(query) ||
|
||||
deskripsi.contains(query) ||
|
||||
kategori.contains(query);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
switch (sortBy.value) {
|
||||
case 'Nama (A-Z)':
|
||||
filtered.sort(
|
||||
(a, b) => a['nama'].toString().compareTo(b['nama'].toString()),
|
||||
);
|
||||
break;
|
||||
case 'Nama (Z-A)':
|
||||
filtered.sort(
|
||||
(a, b) => b['nama'].toString().compareTo(a['nama'].toString()),
|
||||
);
|
||||
break;
|
||||
case 'Harga (Rendah-Tinggi)':
|
||||
filtered.sort((a, b) => a['harga'].compareTo(b['harga']));
|
||||
break;
|
||||
case 'Harga (Tinggi-Rendah)':
|
||||
filtered.sort((a, b) => b['harga'].compareTo(a['harga']));
|
||||
break;
|
||||
}
|
||||
|
||||
// Update filtered list
|
||||
filteredAsetList.assignAll(filtered);
|
||||
}
|
||||
|
||||
// Change tab (Sewa or Langganan)
|
||||
void changeTab(int index) {
|
||||
selectedTabIndex.value = index;
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
// Set search query
|
||||
void setSearchQuery(String query) {
|
||||
searchQuery.value = query;
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
// Set sort option
|
||||
void setSortBy(String option) {
|
||||
sortBy.value = option;
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
// Format price to Indonesian Rupiah
|
||||
String formatPrice(int price) {
|
||||
return 'Rp${price.toString().replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}';
|
||||
}
|
||||
|
||||
// Add a new asset
|
||||
void addAset(Map<String, dynamic> newAset) {
|
||||
// In a real app, this would be an API call
|
||||
// For demo, we'll just add to the list
|
||||
asetList.add(newAset);
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
// Update an existing asset
|
||||
void updateAset(String id, Map<String, dynamic> updatedData) {
|
||||
final index = asetList.indexWhere((aset) => aset['id'] == id);
|
||||
if (index != -1) {
|
||||
asetList[index] = updatedData;
|
||||
applyFilters();
|
||||
}
|
||||
}
|
||||
|
||||
// Delete an asset
|
||||
void deleteAset(String id) {
|
||||
asetList.removeWhere((aset) => aset['id'] == id);
|
||||
applyFilters();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,217 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PetugasBumdesCbpController extends GetxController {
|
||||
// Observable variables
|
||||
final isLoading = true.obs;
|
||||
|
||||
// Bank account data
|
||||
final bankAccounts =
|
||||
<Map<String, dynamic>>[
|
||||
{
|
||||
'id': '1',
|
||||
'bank_name': 'Bank BRI',
|
||||
'account_number': '1234-5678-9101',
|
||||
'account_holder': 'BUMDes CBP Sukamaju',
|
||||
'is_primary': true,
|
||||
},
|
||||
{
|
||||
'id': '2',
|
||||
'bank_name': 'Bank BNI',
|
||||
'account_number': '9876-5432-1098',
|
||||
'account_holder': 'BUMDes CBP Sukamaju',
|
||||
'is_primary': false,
|
||||
},
|
||||
].obs;
|
||||
|
||||
// Partners data
|
||||
final partners =
|
||||
<Map<String, dynamic>>[
|
||||
{
|
||||
'id': '1',
|
||||
'name': 'UD Maju Jaya',
|
||||
'contact': '081234567890',
|
||||
'address': 'Jl. Raya Sukamaju No. 123',
|
||||
'is_active': true,
|
||||
},
|
||||
{
|
||||
'id': '2',
|
||||
'name': 'CV Tani Mandiri',
|
||||
'contact': '087654321098',
|
||||
'address': 'Jl. Kelapa Dua No. 45',
|
||||
'is_active': true,
|
||||
},
|
||||
{
|
||||
'id': '3',
|
||||
'name': 'PT Karya Sejahtera',
|
||||
'contact': '089876543210',
|
||||
'address': 'Jl. Industri Blok C No. 7',
|
||||
'is_active': false,
|
||||
},
|
||||
].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadData();
|
||||
}
|
||||
|
||||
Future<void> loadData() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
// Simulate API delay
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
// Data is already loaded in the initialized lists
|
||||
} catch (e) {
|
||||
print('Error loading data: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memuat data. Silakan coba lagi.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Bank Account Methods
|
||||
void setPrimaryBankAccount(String id) {
|
||||
final index = bankAccounts.indexWhere((account) => account['id'] == id);
|
||||
if (index != -1) {
|
||||
// First, set all accounts to non-primary
|
||||
for (int i = 0; i < bankAccounts.length; i++) {
|
||||
final account = Map<String, dynamic>.from(bankAccounts[i]);
|
||||
account['is_primary'] = false;
|
||||
bankAccounts[i] = account;
|
||||
}
|
||||
|
||||
// Then set the selected account as primary
|
||||
final account = Map<String, dynamic>.from(bankAccounts[index]);
|
||||
account['is_primary'] = true;
|
||||
bankAccounts[index] = account;
|
||||
|
||||
Get.snackbar(
|
||||
'Rekening Utama',
|
||||
'Rekening ${account['bank_name']} telah dijadikan rekening utama',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void addBankAccount(Map<String, dynamic> account) {
|
||||
// Generate a new ID (in a real app, this would be from the backend)
|
||||
account['id'] = (bankAccounts.length + 1).toString();
|
||||
|
||||
// By default, new accounts are not primary
|
||||
account['is_primary'] = false;
|
||||
|
||||
bankAccounts.add(account);
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Rekening Ditambahkan',
|
||||
'Rekening bank baru telah berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
void updateBankAccount(String id, Map<String, dynamic> updatedAccount) {
|
||||
final index = bankAccounts.indexWhere((account) => account['id'] == id);
|
||||
if (index != -1) {
|
||||
// Preserve the ID and primary status
|
||||
updatedAccount['id'] = id;
|
||||
updatedAccount['is_primary'] = bankAccounts[index]['is_primary'];
|
||||
|
||||
bankAccounts[index] = updatedAccount;
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Rekening Diperbarui',
|
||||
'Informasi rekening bank telah berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void deleteBankAccount(String id) {
|
||||
final index = bankAccounts.indexWhere((account) => account['id'] == id);
|
||||
if (index != -1) {
|
||||
// Check if trying to delete the primary account
|
||||
if (bankAccounts[index]['is_primary'] == true) {
|
||||
Get.snackbar(
|
||||
'Tidak Dapat Menghapus',
|
||||
'Rekening utama tidak dapat dihapus. Silakan atur rekening lain sebagai utama terlebih dahulu.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
bankAccounts.removeAt(index);
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Rekening Dihapus',
|
||||
'Rekening bank telah berhasil dihapus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Partner Methods
|
||||
void togglePartnerStatus(String id) {
|
||||
final index = partners.indexWhere((partner) => partner['id'] == id);
|
||||
if (index != -1) {
|
||||
final partner = Map<String, dynamic>.from(partners[index]);
|
||||
partner['is_active'] = !partner['is_active'];
|
||||
partners[index] = partner;
|
||||
|
||||
Get.snackbar(
|
||||
'Status Diperbarui',
|
||||
'Status mitra telah diubah menjadi ${partner['is_active'] ? 'Aktif' : 'Tidak Aktif'}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void addPartner(Map<String, dynamic> partner) {
|
||||
// Generate a new ID (in a real app, this would be from the backend)
|
||||
partner['id'] = (partners.length + 1).toString();
|
||||
|
||||
// By default, new partners are active
|
||||
partner['is_active'] = true;
|
||||
|
||||
partners.add(partner);
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Mitra Ditambahkan',
|
||||
'Mitra baru telah berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
void updatePartner(String id, Map<String, dynamic> updatedPartner) {
|
||||
final index = partners.indexWhere((partner) => partner['id'] == id);
|
||||
if (index != -1) {
|
||||
// Preserve the ID and active status
|
||||
updatedPartner['id'] = id;
|
||||
updatedPartner['is_active'] = partners[index]['is_active'];
|
||||
|
||||
partners[index] = updatedPartner;
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Mitra Diperbarui',
|
||||
'Informasi mitra telah berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void deletePartner(String id) {
|
||||
final index = partners.indexWhere((partner) => partner['id'] == id);
|
||||
if (index != -1) {
|
||||
partners.removeAt(index);
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Mitra Dihapus',
|
||||
'Mitra telah berhasil dihapus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class PetugasBumdesDashboardController extends GetxController {
|
||||
AuthProvider? _authProvider;
|
||||
|
||||
// Reactive variables
|
||||
final userEmail = ''.obs;
|
||||
final currentTabIndex = 0.obs;
|
||||
|
||||
// Revenue Statistics
|
||||
final totalPendapatanBulanIni = 'Rp 8.500.000'.obs;
|
||||
final totalPendapatanBulanLalu = 'Rp 7.200.000'.obs;
|
||||
final persentaseKenaikan = '18%'.obs;
|
||||
final isKenaikanPositif = true.obs;
|
||||
|
||||
// Revenue by Category
|
||||
final pendapatanSewa = 'Rp 5.200.000'.obs;
|
||||
final persentaseSewa = 100.obs;
|
||||
|
||||
// Revenue Trends (last 6 months)
|
||||
final trendPendapatan = [4.2, 5.1, 4.8, 6.2, 7.2, 8.5].obs; // in millions
|
||||
|
||||
// Status Counters for Sewa Aset
|
||||
final terlaksanaCount = 5.obs;
|
||||
final dijadwalkanCount = 1.obs;
|
||||
final aktifCount = 1.obs;
|
||||
final dibatalkanCount = 3.obs;
|
||||
|
||||
// Additional Sewa Aset Status Counters
|
||||
final menungguPembayaranCount = 2.obs;
|
||||
final periksaPembayaranCount = 1.obs;
|
||||
final diterimaCount = 3.obs;
|
||||
final pembayaranDendaCount = 1.obs;
|
||||
final periksaPembayaranDendaCount = 0.obs;
|
||||
final selesaiCount = 4.obs;
|
||||
|
||||
// Status counts for Sewa
|
||||
final pengajuanSewaCount = 5.obs;
|
||||
final pemasanganCountSewa = 3.obs;
|
||||
final sewaAktifCount = 10.obs;
|
||||
final tagihanAktifCountSewa = 7.obs;
|
||||
final periksaPembayaranCountSewa = 2.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
try {
|
||||
_authProvider = Get.find<AuthProvider>();
|
||||
userEmail.value = _authProvider?.currentUser?.email ?? 'Tidak ada email';
|
||||
} catch (e) {
|
||||
print('Error finding AuthProvider: $e');
|
||||
userEmail.value = 'Tidak ada email';
|
||||
}
|
||||
|
||||
// In a real app, these counts would be fetched from backend
|
||||
// loadStatusCounts();
|
||||
print('✅ PetugasBumdesDashboardController initialized successfully');
|
||||
}
|
||||
|
||||
// Method to load status counts from backend
|
||||
// Future<void> loadStatusCounts() async {
|
||||
// try {
|
||||
// final response = await _asetProvider.getSewaStatusCounts();
|
||||
// if (response != null) {
|
||||
// terlaksanaCount.value = response['terlaksana'] ?? 0;
|
||||
// dijadwalkanCount.value = response['dijadwalkan'] ?? 0;
|
||||
// aktifCount.value = response['aktif'] ?? 0;
|
||||
// dibatalkanCount.value = response['dibatalkan'] ?? 0;
|
||||
// menungguPembayaranCount.value = response['menunggu_pembayaran'] ?? 0;
|
||||
// periksaPembayaranCount.value = response['periksa_pembayaran'] ?? 0;
|
||||
// diterimaCount.value = response['diterima'] ?? 0;
|
||||
// pembayaranDendaCount.value = response['pembayaran_denda'] ?? 0;
|
||||
// periksaPembayaranDendaCount.value = response['periksa_pembayaran_denda'] ?? 0;
|
||||
// selesaiCount.value = response['selesai'] ?? 0;
|
||||
// }
|
||||
// } catch (e) {
|
||||
// print('Error loading status counts: $e');
|
||||
// }
|
||||
// }
|
||||
|
||||
void changeTab(int index) {
|
||||
try {
|
||||
currentTabIndex.value = index;
|
||||
|
||||
// Navigate to the appropriate page based on the tab index
|
||||
switch (index) {
|
||||
case 0:
|
||||
// Navigate to Dashboard
|
||||
Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
|
||||
break;
|
||||
case 1:
|
||||
// Navigate to Aset page
|
||||
navigateToAset();
|
||||
break;
|
||||
case 2:
|
||||
// Navigate to Paket page
|
||||
navigateToPaket();
|
||||
break;
|
||||
case 3:
|
||||
// Navigate to Sewa page
|
||||
navigateToSewa();
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error changing tab: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void navigateToAset() {
|
||||
try {
|
||||
Get.offAllNamed(Routes.PETUGAS_ASET);
|
||||
} catch (e) {
|
||||
print('Error navigating to Aset: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void navigateToPaket() {
|
||||
try {
|
||||
Get.offAllNamed(Routes.PETUGAS_PAKET);
|
||||
} catch (e) {
|
||||
print('Error navigating to Paket: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void navigateToSewa() {
|
||||
try {
|
||||
Get.offAllNamed(Routes.PETUGAS_SEWA);
|
||||
} catch (e) {
|
||||
print('Error navigating to Sewa: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void logout() async {
|
||||
try {
|
||||
if (_authProvider != null) {
|
||||
await _authProvider!.signOut();
|
||||
}
|
||||
Get.offAllNamed(Routes.LOGIN);
|
||||
} catch (e) {
|
||||
print('Error during logout: $e');
|
||||
// Still try to navigate to login even if sign out fails
|
||||
Get.offAllNamed(Routes.LOGIN);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,183 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PetugasManajemenBumdesController extends GetxController {
|
||||
// Reactive variables
|
||||
final RxInt selectedTabIndex = 0.obs;
|
||||
final RxBool isLoading = false.obs;
|
||||
|
||||
// Tab options
|
||||
final List<String> tabOptions = ['Akun Bank', 'Mitra'];
|
||||
|
||||
// Sample data for Bank Accounts
|
||||
final RxList<Map<String, dynamic>> bankAccounts =
|
||||
<Map<String, dynamic>>[
|
||||
{
|
||||
'bankName': 'Bank BRI',
|
||||
'accountName': 'BUMDes Sejahtera',
|
||||
'accountNumber': '123456789',
|
||||
'isPrimary': true,
|
||||
},
|
||||
{
|
||||
'bankName': 'Bank BNI',
|
||||
'accountName': 'BUMDes Sejahtera',
|
||||
'accountNumber': '987654321',
|
||||
'isPrimary': false,
|
||||
},
|
||||
].obs;
|
||||
|
||||
// Sample data for Partners
|
||||
final RxList<Map<String, dynamic>> partners =
|
||||
<Map<String, dynamic>>[
|
||||
{
|
||||
'name': 'CV Maju Jaya',
|
||||
'email': 'majujaya@example.com',
|
||||
'phone': '081234567890',
|
||||
'address': 'Jl. Maju No. 123, Kecamatan Berkah',
|
||||
'isActive': true,
|
||||
},
|
||||
{
|
||||
'name': 'PT Sentosa',
|
||||
'email': 'sentosa@example.com',
|
||||
'phone': '089876543210',
|
||||
'address': 'Jl. Sentosa No. 456, Kecamatan Damai',
|
||||
'isActive': false,
|
||||
},
|
||||
].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadData();
|
||||
}
|
||||
|
||||
void loadData() {
|
||||
isLoading.value = true;
|
||||
// Simulate loading data from API
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
// Data already loaded with sample data
|
||||
isLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
void changeTab(int index) {
|
||||
selectedTabIndex.value = index;
|
||||
}
|
||||
|
||||
void setPrimaryBankAccount(int index) {
|
||||
// Set all accounts to non-primary first
|
||||
for (var i = 0; i < bankAccounts.length; i++) {
|
||||
bankAccounts[i]['isPrimary'] = false;
|
||||
}
|
||||
|
||||
// Set the selected account as primary
|
||||
bankAccounts[index]['isPrimary'] = true;
|
||||
|
||||
// Force UI refresh
|
||||
bankAccounts.refresh();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Rekening utama berhasil diubah',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
void togglePartnerStatus(int index) {
|
||||
// Toggle the active status
|
||||
partners[index]['isActive'] = !partners[index]['isActive'];
|
||||
|
||||
// Force UI refresh
|
||||
partners.refresh();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Status mitra berhasil diubah',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
void addBankAccount(Map<String, dynamic> account) {
|
||||
// Set as primary if it's the first account
|
||||
if (bankAccounts.isEmpty) {
|
||||
account['isPrimary'] = true;
|
||||
} else {
|
||||
account['isPrimary'] = false;
|
||||
}
|
||||
|
||||
bankAccounts.add(account);
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Rekening bank berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
void updateBankAccount(int index, Map<String, dynamic> updatedAccount) {
|
||||
// Preserve the primary status
|
||||
updatedAccount['isPrimary'] = bankAccounts[index]['isPrimary'];
|
||||
|
||||
bankAccounts[index] = updatedAccount;
|
||||
bankAccounts.refresh();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Rekening bank berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
void deleteBankAccount(int index) {
|
||||
// Check if the account to be deleted is primary
|
||||
final isPrimary = bankAccounts[index]['isPrimary'];
|
||||
|
||||
// Remove the account
|
||||
bankAccounts.removeAt(index);
|
||||
|
||||
// If the deleted account was primary and there are other accounts, set the first one as primary
|
||||
if (isPrimary && bankAccounts.isNotEmpty) {
|
||||
bankAccounts[0]['isPrimary'] = true;
|
||||
}
|
||||
|
||||
bankAccounts.refresh();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Rekening bank berhasil dihapus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
void addPartner(Map<String, dynamic> partner) {
|
||||
partners.add(partner);
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Mitra berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
void updatePartner(int index, Map<String, dynamic> updatedPartner) {
|
||||
partners[index] = updatedPartner;
|
||||
partners.refresh();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Mitra berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
void deletePartner(int index) {
|
||||
partners.removeAt(index);
|
||||
partners.refresh();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Mitra berhasil dihapus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,253 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class PetugasPaketController extends GetxController {
|
||||
final isLoading = false.obs;
|
||||
final searchQuery = ''.obs;
|
||||
final selectedCategory = 'Semua'.obs;
|
||||
final sortBy = 'Terbaru'.obs;
|
||||
|
||||
// Kategori untuk filter
|
||||
final categories = <String>[
|
||||
'Semua',
|
||||
'Pesta',
|
||||
'Rapat',
|
||||
'Olahraga',
|
||||
'Pernikahan',
|
||||
'Lainnya',
|
||||
];
|
||||
|
||||
// Opsi pengurutan
|
||||
final sortOptions = <String>[
|
||||
'Terbaru',
|
||||
'Terlama',
|
||||
'Harga Tertinggi',
|
||||
'Harga Terendah',
|
||||
'Nama A-Z',
|
||||
'Nama Z-A',
|
||||
];
|
||||
|
||||
// Data dummy paket
|
||||
final paketList = <Map<String, dynamic>>[].obs;
|
||||
final filteredPaketList = <Map<String, dynamic>>[].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadPaketData();
|
||||
}
|
||||
|
||||
// Format harga ke Rupiah
|
||||
String formatPrice(int price) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
return formatter.format(price);
|
||||
}
|
||||
|
||||
// Load data paket dummy
|
||||
Future<void> loadPaketData() async {
|
||||
isLoading.value = true;
|
||||
await Future.delayed(const Duration(milliseconds: 800)); // Simulasi loading
|
||||
|
||||
paketList.value = [
|
||||
{
|
||||
'id': '1',
|
||||
'nama': 'Paket Pesta Ulang Tahun',
|
||||
'kategori': 'Pesta',
|
||||
'harga': 500000,
|
||||
'deskripsi':
|
||||
'Paket lengkap untuk acara ulang tahun. Termasuk 5 meja, 20 kursi, backdrop, dan sound system.',
|
||||
'tersedia': true,
|
||||
'created_at': '2023-08-10',
|
||||
'items': [
|
||||
{'nama': 'Meja Panjang', 'jumlah': 5},
|
||||
{'nama': 'Kursi Plastik', 'jumlah': 20},
|
||||
{'nama': 'Sound System', 'jumlah': 1},
|
||||
{'nama': 'Backdrop', 'jumlah': 1},
|
||||
],
|
||||
'gambar': 'https://example.com/images/paket_ultah.jpg',
|
||||
},
|
||||
{
|
||||
'id': '2',
|
||||
'nama': 'Paket Rapat Sedang',
|
||||
'kategori': 'Rapat',
|
||||
'harga': 300000,
|
||||
'deskripsi':
|
||||
'Paket untuk rapat sedang. Termasuk 1 meja rapat besar, 10 kursi, proyektor, dan screen.',
|
||||
'tersedia': true,
|
||||
'created_at': '2023-09-05',
|
||||
'items': [
|
||||
{'nama': 'Meja Rapat', 'jumlah': 1},
|
||||
{'nama': 'Kursi Kantor', 'jumlah': 10},
|
||||
{'nama': 'Proyektor', 'jumlah': 1},
|
||||
{'nama': 'Screen', 'jumlah': 1},
|
||||
],
|
||||
'gambar': 'https://example.com/images/paket_rapat.jpg',
|
||||
},
|
||||
{
|
||||
'id': '3',
|
||||
'nama': 'Paket Pesta Pernikahan',
|
||||
'kategori': 'Pernikahan',
|
||||
'harga': 1500000,
|
||||
'deskripsi':
|
||||
'Paket lengkap untuk acara pernikahan. Termasuk 20 meja, 100 kursi, sound system, dekorasi, dan tenda.',
|
||||
'tersedia': true,
|
||||
'created_at': '2023-10-12',
|
||||
'items': [
|
||||
{'nama': 'Meja Bundar', 'jumlah': 20},
|
||||
{'nama': 'Kursi Tamu', 'jumlah': 100},
|
||||
{'nama': 'Sound System Besar', 'jumlah': 1},
|
||||
{'nama': 'Tenda 10x10', 'jumlah': 2},
|
||||
{'nama': 'Set Dekorasi Pengantin', 'jumlah': 1},
|
||||
],
|
||||
'gambar': 'https://example.com/images/paket_nikah.jpg',
|
||||
},
|
||||
{
|
||||
'id': '4',
|
||||
'nama': 'Paket Olahraga Voli',
|
||||
'kategori': 'Olahraga',
|
||||
'harga': 200000,
|
||||
'deskripsi':
|
||||
'Paket perlengkapan untuk turnamen voli. Termasuk net, bola, dan tiang voli.',
|
||||
'tersedia': false,
|
||||
'created_at': '2023-07-22',
|
||||
'items': [
|
||||
{'nama': 'Net Voli', 'jumlah': 1},
|
||||
{'nama': 'Bola Voli', 'jumlah': 3},
|
||||
{'nama': 'Tiang Voli', 'jumlah': 2},
|
||||
],
|
||||
'gambar': 'https://example.com/images/paket_voli.jpg',
|
||||
},
|
||||
{
|
||||
'id': '5',
|
||||
'nama': 'Paket Pesta Anak',
|
||||
'kategori': 'Pesta',
|
||||
'harga': 350000,
|
||||
'deskripsi':
|
||||
'Paket untuk pesta ulang tahun anak-anak. Termasuk 3 meja, 15 kursi, dekorasi tema, dan sound system kecil.',
|
||||
'tersedia': true,
|
||||
'created_at': '2023-11-01',
|
||||
'items': [
|
||||
{'nama': 'Meja Anak', 'jumlah': 3},
|
||||
{'nama': 'Kursi Anak', 'jumlah': 15},
|
||||
{'nama': 'Set Dekorasi Tema', 'jumlah': 1},
|
||||
{'nama': 'Sound System Kecil', 'jumlah': 1},
|
||||
],
|
||||
'gambar': 'https://example.com/images/paket_anak.jpg',
|
||||
},
|
||||
];
|
||||
|
||||
filterPaket();
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
// Filter paket berdasarkan search query dan kategori
|
||||
void filterPaket() {
|
||||
filteredPaketList.value =
|
||||
paketList.where((paket) {
|
||||
final matchesQuery =
|
||||
paket['nama'].toString().toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
) ||
|
||||
paket['deskripsi'].toString().toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
);
|
||||
|
||||
final matchesCategory =
|
||||
selectedCategory.value == 'Semua' ||
|
||||
paket['kategori'] == selectedCategory.value;
|
||||
|
||||
return matchesQuery && matchesCategory;
|
||||
}).toList();
|
||||
|
||||
// Sort the filtered list
|
||||
sortFilteredList();
|
||||
}
|
||||
|
||||
// Sort the filtered list
|
||||
void sortFilteredList() {
|
||||
switch (sortBy.value) {
|
||||
case 'Terbaru':
|
||||
filteredPaketList.sort(
|
||||
(a, b) => b['created_at'].compareTo(a['created_at']),
|
||||
);
|
||||
break;
|
||||
case 'Terlama':
|
||||
filteredPaketList.sort(
|
||||
(a, b) => a['created_at'].compareTo(b['created_at']),
|
||||
);
|
||||
break;
|
||||
case 'Harga Tertinggi':
|
||||
filteredPaketList.sort((a, b) => b['harga'].compareTo(a['harga']));
|
||||
break;
|
||||
case 'Harga Terendah':
|
||||
filteredPaketList.sort((a, b) => a['harga'].compareTo(b['harga']));
|
||||
break;
|
||||
case 'Nama A-Z':
|
||||
filteredPaketList.sort((a, b) => a['nama'].compareTo(b['nama']));
|
||||
break;
|
||||
case 'Nama Z-A':
|
||||
filteredPaketList.sort((a, b) => b['nama'].compareTo(a['nama']));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Set search query dan filter paket
|
||||
void setSearchQuery(String query) {
|
||||
searchQuery.value = query;
|
||||
filterPaket();
|
||||
}
|
||||
|
||||
// Set kategori dan filter paket
|
||||
void setCategory(String category) {
|
||||
selectedCategory.value = category;
|
||||
filterPaket();
|
||||
}
|
||||
|
||||
// Set opsi pengurutan dan filter paket
|
||||
void setSortBy(String option) {
|
||||
sortBy.value = option;
|
||||
sortFilteredList();
|
||||
}
|
||||
|
||||
// Tambah paket baru
|
||||
void addPaket(Map<String, dynamic> paket) {
|
||||
paketList.add(paket);
|
||||
filterPaket();
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Paket baru berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
// Edit paket
|
||||
void editPaket(String id, Map<String, dynamic> updatedPaket) {
|
||||
final index = paketList.indexWhere((element) => element['id'] == id);
|
||||
if (index >= 0) {
|
||||
paketList[index] = updatedPaket;
|
||||
filterPaket();
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Paket berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Hapus paket
|
||||
void deletePaket(String id) {
|
||||
paketList.removeWhere((element) => element['id'] == id);
|
||||
filterPaket();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Paket berhasil dihapus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,314 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PetugasSewaController extends GetxController {
|
||||
// Reactive variables
|
||||
final isLoading = true.obs;
|
||||
final searchQuery = ''.obs;
|
||||
final orderIdQuery = ''.obs;
|
||||
final selectedStatusFilter = 'Semua'.obs;
|
||||
final filteredSewaList = <Map<String, dynamic>>[].obs;
|
||||
|
||||
// Filter options
|
||||
final List<String> statusFilters = [
|
||||
'Semua',
|
||||
'Menunggu Pembayaran',
|
||||
'Periksa Pembayaran',
|
||||
'Diterima',
|
||||
'Dikembalikan',
|
||||
'Selesai',
|
||||
'Dibatalkan',
|
||||
];
|
||||
|
||||
// Mock data for sewa list
|
||||
final RxList<Map<String, dynamic>> sewaList = <Map<String, dynamic>>[].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// Add listeners to update filtered list when any filter changes
|
||||
ever(searchQuery, (_) => _updateFilteredList());
|
||||
ever(orderIdQuery, (_) => _updateFilteredList());
|
||||
ever(selectedStatusFilter, (_) => _updateFilteredList());
|
||||
ever(sewaList, (_) => _updateFilteredList());
|
||||
|
||||
// Load initial data
|
||||
loadSewaData();
|
||||
}
|
||||
|
||||
// Update filtered list based on current filters
|
||||
void _updateFilteredList() {
|
||||
filteredSewaList.value =
|
||||
sewaList.where((sewa) {
|
||||
// Apply search filter
|
||||
final matchesSearch = sewa['nama_warga']
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.contains(searchQuery.value.toLowerCase());
|
||||
|
||||
// Apply order ID filter if provided
|
||||
final matchesOrderId =
|
||||
orderIdQuery.value.isEmpty ||
|
||||
sewa['order_id'].toString().toLowerCase().contains(
|
||||
orderIdQuery.value.toLowerCase(),
|
||||
);
|
||||
|
||||
// Apply status filter if not 'Semua'
|
||||
final matchesStatus =
|
||||
selectedStatusFilter.value == 'Semua' ||
|
||||
sewa['status'] == selectedStatusFilter.value;
|
||||
|
||||
return matchesSearch && matchesOrderId && matchesStatus;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Load sewa data (mock data for now)
|
||||
Future<void> loadSewaData() async {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// Simulate API call delay
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
|
||||
// Populate with mock data
|
||||
sewaList.assignAll([
|
||||
{
|
||||
'id': '1',
|
||||
'order_id': 'SWA-001',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-02-05',
|
||||
'tanggal_selesai': '2025-02-10',
|
||||
'total_biaya': 45000,
|
||||
'status': 'Diterima',
|
||||
'photo_url': 'https://example.com/photo1.jpg',
|
||||
},
|
||||
{
|
||||
'id': '2',
|
||||
'order_id': 'SWA-002',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-02-15',
|
||||
'tanggal_selesai': '2025-02-20',
|
||||
'total_biaya': 30000,
|
||||
'status': 'Selesai',
|
||||
'photo_url': 'https://example.com/photo2.jpg',
|
||||
},
|
||||
{
|
||||
'id': '3',
|
||||
'order_id': 'SWA-003',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-02-25',
|
||||
'tanggal_selesai': '2025-03-01',
|
||||
'total_biaya': 35000,
|
||||
'status': 'Menunggu Pembayaran',
|
||||
'photo_url': 'https://example.com/photo3.jpg',
|
||||
},
|
||||
{
|
||||
'id': '4',
|
||||
'order_id': 'SWA-004',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-03-05',
|
||||
'tanggal_selesai': '2025-03-08',
|
||||
'total_biaya': 20000,
|
||||
'status': 'Periksa Pembayaran',
|
||||
'photo_url': 'https://example.com/photo4.jpg',
|
||||
},
|
||||
{
|
||||
'id': '5',
|
||||
'order_id': 'SWA-005',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-03-12',
|
||||
'tanggal_selesai': '2025-03-14',
|
||||
'total_biaya': 15000,
|
||||
'status': 'Dibatalkan',
|
||||
'photo_url': 'https://example.com/photo5.jpg',
|
||||
},
|
||||
{
|
||||
'id': '6',
|
||||
'order_id': 'SWA-006',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-03-18',
|
||||
'tanggal_selesai': '2025-03-20',
|
||||
'total_biaya': 25000,
|
||||
'status': 'Pembayaran Denda',
|
||||
'photo_url': 'https://example.com/photo6.jpg',
|
||||
},
|
||||
{
|
||||
'id': '7',
|
||||
'order_id': 'SWA-007',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-03-25',
|
||||
'tanggal_selesai': '2025-03-28',
|
||||
'total_biaya': 40000,
|
||||
'status': 'Periksa Denda',
|
||||
'photo_url': 'https://example.com/photo7.jpg',
|
||||
},
|
||||
{
|
||||
'id': '8',
|
||||
'order_id': 'SWA-008',
|
||||
'nama_warga': 'Sukimin',
|
||||
'nama_aset': 'Mobil Pickup',
|
||||
'tanggal_mulai': '2025-04-02',
|
||||
'tanggal_selesai': '2025-04-05',
|
||||
'total_biaya': 10000,
|
||||
'status': 'Dikembalikan',
|
||||
'photo_url': 'https://example.com/photo8.jpg',
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
print('Error loading sewa data: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update search query
|
||||
void setSearchQuery(String query) {
|
||||
searchQuery.value = query;
|
||||
}
|
||||
|
||||
// Update order ID query
|
||||
void setOrderIdQuery(String query) {
|
||||
orderIdQuery.value = query;
|
||||
}
|
||||
|
||||
// Update status filter
|
||||
void setStatusFilter(String status) {
|
||||
selectedStatusFilter.value = status;
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
void resetFilters() {
|
||||
selectedStatusFilter.value = 'Semua';
|
||||
searchQuery.value = '';
|
||||
filteredSewaList.value = sewaList;
|
||||
}
|
||||
|
||||
void applyFilters() {
|
||||
filteredSewaList.value =
|
||||
sewaList.where((sewa) {
|
||||
bool matchesStatus =
|
||||
selectedStatusFilter.value == 'Semua' ||
|
||||
sewa['status'] == selectedStatusFilter.value;
|
||||
bool matchesSearch =
|
||||
searchQuery.value.isEmpty ||
|
||||
sewa['nama_warga'].toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
);
|
||||
return matchesStatus && matchesSearch;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Format price to rupiah
|
||||
String formatPrice(num price) {
|
||||
return 'Rp ${price.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}';
|
||||
}
|
||||
|
||||
// Get color based on status
|
||||
Color getStatusColor(String status) {
|
||||
switch (status) {
|
||||
case 'Menunggu Pembayaran':
|
||||
return Colors.orange;
|
||||
case 'Periksa Pembayaran':
|
||||
return Colors.amber.shade700;
|
||||
case 'Diterima':
|
||||
return Colors.blue;
|
||||
case 'Pembayaran Denda':
|
||||
return Colors.deepOrange;
|
||||
case 'Periksa Denda':
|
||||
return Colors.red.shade600;
|
||||
case 'Dikembalikan':
|
||||
return Colors.teal;
|
||||
case 'Sedang Disewa':
|
||||
return Colors.green;
|
||||
case 'Selesai':
|
||||
return Colors.purple;
|
||||
case 'Dibatalkan':
|
||||
return Colors.red;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = Map<String, dynamic>.from(sewaList[index]);
|
||||
final currentStatus = sewa['status'];
|
||||
|
||||
if (currentStatus == 'Periksa Pembayaran') {
|
||||
sewa['status'] = 'Diterima';
|
||||
} else if (currentStatus == 'Periksa Denda') {
|
||||
sewa['status'] = 'Selesai';
|
||||
} else if (currentStatus == 'Menunggu Pembayaran') {
|
||||
sewa['status'] = 'Periksa Pembayaran';
|
||||
}
|
||||
|
||||
sewaList[index] = sewa;
|
||||
sewaList.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sewa rejection or cancellation
|
||||
void rejectSewa(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Dibatalkan';
|
||||
sewaList[index] = sewa;
|
||||
sewaList.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Request payment for penalty
|
||||
void requestPenaltyPayment(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Pembayaran Denda';
|
||||
sewaList[index] = sewa;
|
||||
sewaList.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Mark penalty payment as requiring inspection
|
||||
void markPenaltyForInspection(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Periksa Denda';
|
||||
sewaList[index] = sewa;
|
||||
sewaList.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sewa completion
|
||||
void completeSewa(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Selesai';
|
||||
sewaList[index] = sewa;
|
||||
sewaList.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Mark rental as returned
|
||||
void markAsReturned(String id) {
|
||||
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
|
||||
if (index != -1) {
|
||||
final sewa = Map<String, dynamic>.from(sewaList[index]);
|
||||
sewa['status'] = 'Dikembalikan';
|
||||
sewaList[index] = sewa;
|
||||
sewaList.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,210 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PetugasTambahAsetController extends GetxController {
|
||||
// Form controllers
|
||||
final nameController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
final quantityController = TextEditingController();
|
||||
final unitOfMeasureController = TextEditingController();
|
||||
final pricePerHourController = TextEditingController();
|
||||
final maxHourController = TextEditingController();
|
||||
final pricePerDayController = TextEditingController();
|
||||
final maxDayController = TextEditingController();
|
||||
|
||||
// Dropdown and toggle values
|
||||
final selectedCategory = 'Sewa'.obs;
|
||||
final selectedStatus = 'Tersedia'.obs;
|
||||
|
||||
// Replace single selection with multiple selections
|
||||
final timeOptions = {'Per Jam': true.obs, 'Per Hari': false.obs};
|
||||
|
||||
// Category options
|
||||
final categoryOptions = ['Sewa', 'Langganan'];
|
||||
final statusOptions = ['Tersedia', 'Pemeliharaan'];
|
||||
|
||||
// Images
|
||||
final selectedImages = <String>[].obs;
|
||||
|
||||
// Form validation
|
||||
final isFormValid = false.obs;
|
||||
final isSubmitting = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// Set default values
|
||||
quantityController.text = '1';
|
||||
unitOfMeasureController.text = 'Unit';
|
||||
|
||||
// Listen to field changes for validation
|
||||
nameController.addListener(validateForm);
|
||||
descriptionController.addListener(validateForm);
|
||||
quantityController.addListener(validateForm);
|
||||
pricePerHourController.addListener(validateForm);
|
||||
pricePerDayController.addListener(validateForm);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// Dispose controllers
|
||||
nameController.dispose();
|
||||
descriptionController.dispose();
|
||||
quantityController.dispose();
|
||||
unitOfMeasureController.dispose();
|
||||
pricePerHourController.dispose();
|
||||
maxHourController.dispose();
|
||||
pricePerDayController.dispose();
|
||||
maxDayController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Change selected category
|
||||
void setCategory(String category) {
|
||||
selectedCategory.value = category;
|
||||
validateForm();
|
||||
}
|
||||
|
||||
// Change selected status
|
||||
void setStatus(String status) {
|
||||
selectedStatus.value = status;
|
||||
validateForm();
|
||||
}
|
||||
|
||||
// Toggle time option
|
||||
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 none selected, force this one to remain selected
|
||||
if (!anySelected) {
|
||||
timeOptions[option]?.value = true;
|
||||
}
|
||||
|
||||
validateForm();
|
||||
}
|
||||
|
||||
// Add image to the list (in a real app, this would handle file upload)
|
||||
void addImage(String imagePath) {
|
||||
selectedImages.add(imagePath);
|
||||
validateForm();
|
||||
}
|
||||
|
||||
// Remove image from the list
|
||||
void removeImage(int index) {
|
||||
if (index >= 0 && index < selectedImages.length) {
|
||||
selectedImages.removeAt(index);
|
||||
validateForm();
|
||||
}
|
||||
}
|
||||
|
||||
// Validate form fields
|
||||
void validateForm() {
|
||||
// Basic validation
|
||||
bool basicValid =
|
||||
nameController.text.isNotEmpty &&
|
||||
descriptionController.text.isNotEmpty &&
|
||||
quantityController.text.isNotEmpty &&
|
||||
int.tryParse(quantityController.text) != null;
|
||||
|
||||
// Time option validation
|
||||
bool perHourValid =
|
||||
!timeOptions['Per Jam']!.value ||
|
||||
(pricePerHourController.text.isNotEmpty &&
|
||||
int.tryParse(pricePerHourController.text) != null);
|
||||
|
||||
bool perDayValid =
|
||||
!timeOptions['Per Hari']!.value ||
|
||||
(pricePerDayController.text.isNotEmpty &&
|
||||
int.tryParse(pricePerDayController.text) != null);
|
||||
|
||||
// At least one time option must be selected
|
||||
bool anyTimeOptionSelected = false;
|
||||
timeOptions.forEach((key, value) {
|
||||
if (value.value) anyTimeOptionSelected = true;
|
||||
});
|
||||
|
||||
isFormValid.value =
|
||||
basicValid && perHourValid && perDayValid && anyTimeOptionSelected;
|
||||
}
|
||||
|
||||
// Submit form and save asset
|
||||
Future<void> saveAsset() async {
|
||||
if (!isFormValid.value) return;
|
||||
|
||||
isSubmitting.value = true;
|
||||
|
||||
try {
|
||||
// In a real app, this would make an API call to save the asset
|
||||
await Future.delayed(const Duration(seconds: 1)); // Mock API call
|
||||
|
||||
// Prepare asset data
|
||||
final assetData = {
|
||||
'nama': nameController.text,
|
||||
'deskripsi': descriptionController.text,
|
||||
'kategori': selectedCategory.value,
|
||||
'status': selectedStatus.value,
|
||||
'kuantitas': int.parse(quantityController.text),
|
||||
'satuan_ukur': unitOfMeasureController.text,
|
||||
'opsi_waktu_sewa':
|
||||
timeOptions.entries
|
||||
.where((entry) => entry.value.value)
|
||||
.map((entry) => entry.key)
|
||||
.toList(),
|
||||
'harga_per_jam':
|
||||
timeOptions['Per Jam']!.value
|
||||
? int.parse(pricePerHourController.text)
|
||||
: null,
|
||||
'max_jam':
|
||||
timeOptions['Per Jam']!.value && maxHourController.text.isNotEmpty
|
||||
? int.parse(maxHourController.text)
|
||||
: null,
|
||||
'harga_per_hari':
|
||||
timeOptions['Per Hari']!.value
|
||||
? int.parse(pricePerDayController.text)
|
||||
: null,
|
||||
'max_hari':
|
||||
timeOptions['Per Hari']!.value && maxDayController.text.isNotEmpty
|
||||
? int.parse(maxDayController.text)
|
||||
: null,
|
||||
'gambar': selectedImages,
|
||||
};
|
||||
|
||||
// Log the data (in a real app, this would be sent to an API)
|
||||
print('Asset data: $assetData');
|
||||
|
||||
// Return to the asset list page
|
||||
Get.back();
|
||||
|
||||
// Show success message
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Aset berhasil ditambahkan',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} catch (e) {
|
||||
// Show error message
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan: ${e.toString()}',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// For demonstration purposes: add sample image
|
||||
void addSampleImage() {
|
||||
addImage('assets/images/sample_asset_${selectedImages.length + 1}.jpg');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,393 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PetugasTambahPaketController extends GetxController {
|
||||
// Form controllers
|
||||
final nameController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
final priceController = TextEditingController();
|
||||
final itemQuantityController = TextEditingController();
|
||||
|
||||
// Dropdown and toggle values
|
||||
final selectedCategory = 'Bulanan'.obs;
|
||||
final selectedStatus = 'Aktif'.obs;
|
||||
|
||||
// Category options
|
||||
final categoryOptions = ['Bulanan', 'Tahunan', 'Premium', 'Bisnis'];
|
||||
final statusOptions = ['Aktif', 'Nonaktif'];
|
||||
|
||||
// Images
|
||||
final selectedImages = <String>[].obs;
|
||||
|
||||
// For package name and description
|
||||
final packageNameController = TextEditingController();
|
||||
final packageDescriptionController = TextEditingController();
|
||||
final packagePriceController = TextEditingController();
|
||||
|
||||
// For items/assets in the package
|
||||
final RxList<Map<String, dynamic>> packageItems =
|
||||
<Map<String, dynamic>>[].obs;
|
||||
|
||||
// For asset selection
|
||||
final RxList<Map<String, dynamic>> availableAssets =
|
||||
<Map<String, dynamic>>[].obs;
|
||||
final Rx<int?> selectedAsset = Rx<int?>(null);
|
||||
final RxBool isLoadingAssets = false.obs;
|
||||
|
||||
// Form validation
|
||||
final isFormValid = false.obs;
|
||||
final isSubmitting = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// Listen to field changes for validation
|
||||
nameController.addListener(validateForm);
|
||||
descriptionController.addListener(validateForm);
|
||||
priceController.addListener(validateForm);
|
||||
|
||||
// Load available assets when the controller initializes
|
||||
fetchAvailableAssets();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// Dispose controllers
|
||||
nameController.dispose();
|
||||
descriptionController.dispose();
|
||||
priceController.dispose();
|
||||
itemQuantityController.dispose();
|
||||
packageNameController.dispose();
|
||||
packageDescriptionController.dispose();
|
||||
packagePriceController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Change selected category
|
||||
void setCategory(String category) {
|
||||
selectedCategory.value = category;
|
||||
validateForm();
|
||||
}
|
||||
|
||||
// Change selected status
|
||||
void setStatus(String status) {
|
||||
selectedStatus.value = status;
|
||||
validateForm();
|
||||
}
|
||||
|
||||
// Add image to the list (in a real app, this would handle file upload)
|
||||
void addImage(String imagePath) {
|
||||
selectedImages.add(imagePath);
|
||||
validateForm();
|
||||
}
|
||||
|
||||
// Remove image from the list
|
||||
void removeImage(int index) {
|
||||
if (index >= 0 && index < selectedImages.length) {
|
||||
selectedImages.removeAt(index);
|
||||
validateForm();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch available assets from the API or local data
|
||||
void fetchAvailableAssets() {
|
||||
isLoadingAssets.value = true;
|
||||
|
||||
// This is a mock implementation - replace with actual API call
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
availableAssets.value = [
|
||||
{'id': 1, 'nama': 'Laptop Dell XPS', 'stok': 5},
|
||||
{'id': 2, 'nama': 'Proyektor Epson', 'stok': 3},
|
||||
{'id': 3, 'nama': 'Meja Kantor', 'stok': 10},
|
||||
{'id': 4, 'nama': 'Kursi Ergonomis', 'stok': 15},
|
||||
{'id': 5, 'nama': 'Printer HP LaserJet', 'stok': 2},
|
||||
{'id': 6, 'nama': 'AC Panasonic 1PK', 'stok': 8},
|
||||
];
|
||||
isLoadingAssets.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Set the selected asset
|
||||
void setSelectedAsset(int? assetId) {
|
||||
selectedAsset.value = assetId;
|
||||
}
|
||||
|
||||
// Get remaining stock for an asset (considering current selections)
|
||||
int getRemainingStock(int assetId) {
|
||||
// Find the asset in available assets
|
||||
final asset = availableAssets.firstWhere(
|
||||
(item) => item['id'] == assetId,
|
||||
orElse: () => <String, dynamic>{},
|
||||
);
|
||||
|
||||
if (asset.isEmpty) return 0;
|
||||
|
||||
// Get total stock
|
||||
final totalStock = asset['stok'] as int;
|
||||
|
||||
// Calculate how many of this asset are already in the package
|
||||
int alreadySelected = 0;
|
||||
for (var item in packageItems) {
|
||||
if (item['asetId'] == assetId) {
|
||||
alreadySelected += item['jumlah'] as int;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the remaining available stock
|
||||
return totalStock - alreadySelected;
|
||||
}
|
||||
|
||||
// Add an asset to the package
|
||||
void addAssetToPackage() {
|
||||
if (selectedAsset.value == null || itemQuantityController.text.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Pilih aset dan masukkan jumlah',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the selected asset
|
||||
final asset = availableAssets.firstWhere(
|
||||
(item) => item['id'] == selectedAsset.value,
|
||||
orElse: () => <String, dynamic>{},
|
||||
);
|
||||
|
||||
if (asset.isEmpty) return;
|
||||
|
||||
// Convert quantity to int
|
||||
final quantity = int.tryParse(itemQuantityController.text) ?? 0;
|
||||
if (quantity <= 0) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Jumlah harus lebih dari 0',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if quantity is within limits
|
||||
final remainingStock = getRemainingStock(selectedAsset.value!);
|
||||
if (quantity > remainingStock) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Jumlah melebihi stok yang tersedia',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the item to package
|
||||
packageItems.add({
|
||||
'asetId': selectedAsset.value,
|
||||
'nama': asset['nama'],
|
||||
'jumlah': quantity,
|
||||
'stok': asset['stok'],
|
||||
});
|
||||
|
||||
// Clear selection
|
||||
selectedAsset.value = null;
|
||||
itemQuantityController.clear();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Item berhasil ditambahkan ke paket',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
|
||||
// Update an existing package item
|
||||
void updatePackageItem(int index) {
|
||||
if (selectedAsset.value == null || itemQuantityController.text.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Pilih aset dan masukkan jumlah',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the selected asset
|
||||
final asset = availableAssets.firstWhere(
|
||||
(item) => item['id'] == selectedAsset.value,
|
||||
orElse: () => <String, dynamic>{},
|
||||
);
|
||||
|
||||
if (asset.isEmpty) return;
|
||||
|
||||
// Convert quantity to int
|
||||
final quantity = int.tryParse(itemQuantityController.text) ?? 0;
|
||||
if (quantity <= 0) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Jumlah harus lebih dari 0',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// If updating the same asset, check remaining stock + current quantity
|
||||
final currentItem = packageItems[index];
|
||||
int availableQuantity = asset['stok'] as int;
|
||||
|
||||
// If editing the same asset, we need to consider its current quantity
|
||||
if (currentItem['asetId'] == selectedAsset.value) {
|
||||
// For the same asset, we can reuse its current quantity
|
||||
final alreadyUsed = packageItems
|
||||
.where(
|
||||
(item) =>
|
||||
item['asetId'] == selectedAsset.value &&
|
||||
packageItems.indexOf(item) != index,
|
||||
)
|
||||
.fold(0, (sum, item) => sum + (item['jumlah'] as int));
|
||||
|
||||
availableQuantity -= alreadyUsed;
|
||||
|
||||
if (quantity > availableQuantity) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Jumlah melebihi stok yang tersedia',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// If changing to a different asset, check the new asset's remaining stock
|
||||
final remainingStock = getRemainingStock(selectedAsset.value!);
|
||||
if (quantity > remainingStock) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Jumlah melebihi stok yang tersedia',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the item
|
||||
packageItems[index] = {
|
||||
'asetId': selectedAsset.value,
|
||||
'nama': asset['nama'],
|
||||
'jumlah': quantity,
|
||||
'stok': asset['stok'],
|
||||
};
|
||||
|
||||
// Clear selection
|
||||
selectedAsset.value = null;
|
||||
itemQuantityController.clear();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Item berhasil diperbarui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
|
||||
// Remove an item from the package
|
||||
void removeItem(int index) {
|
||||
packageItems.removeAt(index);
|
||||
Get.snackbar(
|
||||
'Dihapus',
|
||||
'Item berhasil dihapus dari paket',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate form fields
|
||||
void validateForm() {
|
||||
// Basic validation
|
||||
bool basicValid =
|
||||
nameController.text.isNotEmpty &&
|
||||
descriptionController.text.isNotEmpty &&
|
||||
priceController.text.isNotEmpty &&
|
||||
int.tryParse(priceController.text) != null;
|
||||
|
||||
// Package should have at least one item
|
||||
bool hasItems = packageItems.isNotEmpty;
|
||||
|
||||
isFormValid.value = basicValid && hasItems;
|
||||
}
|
||||
|
||||
// Submit form and save package
|
||||
Future<void> savePaket() async {
|
||||
if (!isFormValid.value) return;
|
||||
|
||||
isSubmitting.value = true;
|
||||
|
||||
try {
|
||||
// In a real app, this would make an API call to save the package
|
||||
await Future.delayed(const Duration(seconds: 1)); // Mock API call
|
||||
|
||||
// Prepare package data
|
||||
final paketData = {
|
||||
'nama': nameController.text,
|
||||
'deskripsi': descriptionController.text,
|
||||
'kategori': selectedCategory.value,
|
||||
'status': selectedStatus.value == 'Aktif',
|
||||
'harga': int.parse(priceController.text),
|
||||
'gambar': selectedImages,
|
||||
'items': packageItems,
|
||||
};
|
||||
|
||||
// Log the data (in a real app, this would be sent to an API)
|
||||
print('Package data: $paketData');
|
||||
|
||||
// Return to the package list page
|
||||
Get.back();
|
||||
|
||||
// Show success message
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Paket berhasil ditambahkan',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} catch (e) {
|
||||
// Show error message
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan: ${e.toString()}',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Old sample method (will be replaced)
|
||||
void addSampleItem() {
|
||||
packageItems.add({'nama': 'Laptop Dell XPS', 'jumlah': 1});
|
||||
}
|
||||
|
||||
// Method untuk menambahkan gambar sample
|
||||
void addSampleImage() {
|
||||
// Menambahkan URL gambar dummy untuk keperluan pengembangan
|
||||
selectedImages.add('https://example.com/sample_image.jpg');
|
||||
validateForm();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,581 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/list_pelanggan_aktif_controller.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class ListPelangganAktifView extends GetView<ListPelangganAktifController> {
|
||||
const ListPelangganAktifView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get dashboard controller for navigation
|
||||
final dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
appBar: AppBar(
|
||||
title: Obx(
|
||||
() => Text(
|
||||
'Pelanggan ${controller.serviceName.value}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
actions: [
|
||||
// Actions removed
|
||||
],
|
||||
),
|
||||
drawer: PetugasSideNavbar(controller: dashboardController),
|
||||
drawerEdgeDragWidth: 60,
|
||||
drawerScrimColor: Colors.black.withOpacity(0.6),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
_buildSearchBar(),
|
||||
Expanded(child: _buildSubscribersList()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
offset: const Offset(0, 2),
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(
|
||||
() => Text(
|
||||
'Pelanggan Aktif ${controller.serviceName.value}',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Obx(
|
||||
() => Text(
|
||||
'Daftar warga yang berlangganan ${controller.serviceName.value.toLowerCase()}',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.people_alt_rounded,
|
||||
size: 16,
|
||||
color: Colors.green,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Obx(
|
||||
() => Text(
|
||||
'${controller.pelangganList.length} Pelanggan Aktif',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: Colors.white,
|
||||
child: TextField(
|
||||
onChanged: controller.updateSearchQuery,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari pelanggan...',
|
||||
prefixIcon: const Icon(Icons.search, color: Colors.grey),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubscribersList() {
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.filteredPelangganList.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredPelangganList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final pelanggan = controller.filteredPelangganList[index];
|
||||
return _buildPelangganCard(pelanggan);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPelangganCard(Map<String, dynamic> pelanggan) {
|
||||
final statusColor = Color(controller.getStatusColor(pelanggan['status']));
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
onTap:
|
||||
() => Get.toNamed(
|
||||
Routes.LIST_TAGIHAN_PERIODE,
|
||||
arguments: {
|
||||
'pelanggan': pelanggan,
|
||||
'serviceName': controller.serviceName.value,
|
||||
},
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundColor: AppColorsPetugas.babyBlueBright,
|
||||
child: Text(
|
||||
pelanggan['nama'].substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
pelanggan['nama'],
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
pelanggan['alamat'],
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, size: 14, color: statusColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
pelanggan['status'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildInfoItem(
|
||||
icon: Icons.calendar_month,
|
||||
label: 'Mulai',
|
||||
value: pelanggan['tanggal_mulai'],
|
||||
),
|
||||
_buildInfoItem(
|
||||
icon: Icons.payment,
|
||||
label: 'Tagihan',
|
||||
value: pelanggan['tagihan'],
|
||||
),
|
||||
_buildInfoItem(
|
||||
icon: Icons.phone,
|
||||
label: 'Telepon',
|
||||
value: pelanggan['telepon'],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoItem({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
}) {
|
||||
return Column(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: Colors.grey.shade600),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.people_alt_outlined,
|
||||
size: 60,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Tidak ada pelanggan aktif',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Belum ada warga yang berlangganan layanan ini',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPelangganDetails(Map<String, dynamic> pelanggan) {
|
||||
final statusColor = Color(controller.getStatusColor(pelanggan['status']));
|
||||
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundColor: Colors.white,
|
||||
child: Text(
|
||||
pelanggan['nama'].substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
pelanggan['nama'],
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
pelanggan['alamat'],
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
size: 14,
|
||||
color: statusColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
pelanggan['status'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Detail Pelanggan',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildDetailRow(
|
||||
icon: Icons.calendar_month,
|
||||
label: 'Tanggal Mulai',
|
||||
value: pelanggan['tanggal_mulai'],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.event_busy,
|
||||
label: 'Tanggal Berakhir',
|
||||
value: pelanggan['tanggal_berakhir'],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.payment,
|
||||
label: 'Pembayaran Terakhir',
|
||||
value: pelanggan['pembayaran_terakhir'],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.receipt_long,
|
||||
label: 'Tagihan',
|
||||
value: pelanggan['tagihan'],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Kontak',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildDetailRow(
|
||||
icon: Icons.phone,
|
||||
label: 'Telepon',
|
||||
value: pelanggan['telepon'],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.email,
|
||||
label: 'Email',
|
||||
value: pelanggan['email'],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (pelanggan['catatan'] != null) ...[
|
||||
const Text(
|
||||
'Catatan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
pelanggan['catatan'],
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.close),
|
||||
label: const Text('Tutup'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, size: 16, color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,720 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/list_petugas_mitra_controller.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
|
||||
class ListPetugasMitraView extends GetView<ListPetugasMitraController> {
|
||||
const ListPetugasMitraView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Petugas Mitra',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help_outline),
|
||||
onPressed: () {
|
||||
_showHelpDialog(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Search Bar
|
||||
_buildSearchBar(),
|
||||
|
||||
// List of Partners
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.filteredPartners.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return _buildPartnersList();
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_showAddPartnerDialog(context);
|
||||
},
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: Colors.white,
|
||||
child: TextField(
|
||||
onChanged: (value) {
|
||||
controller.searchQuery.value = value;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari petugas mitra...',
|
||||
prefixIcon: const Icon(Icons.search, color: Colors.grey),
|
||||
suffixIcon: Obx(() {
|
||||
return controller.searchQuery.value.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
controller.searchQuery.value = '';
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.people_outline, size: 80, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Belum ada petugas mitra',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tambahkan petugas mitra dengan menekan tombol "+" di bawah',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPartnersList() {
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredPartners.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final partner = controller.filteredPartners[index];
|
||||
return _buildPartnerCard(partner);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPartnerCard(Map<String, dynamic> partner) {
|
||||
final isActive = partner['is_active'] as bool;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor:
|
||||
isActive ? Colors.green.shade100 : Colors.red.shade100,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color:
|
||||
isActive ? Colors.green.shade700 : Colors.red.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
partner['name'],
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
partner['role'],
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? Colors.green.shade50 : Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
isActive ? 'Aktif' : 'Nonaktif',
|
||||
style: TextStyle(
|
||||
color:
|
||||
isActive
|
||||
? Colors.green.shade700
|
||||
: Colors.red.shade700,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (value) {
|
||||
_handleMenuAction(value, partner);
|
||||
},
|
||||
itemBuilder:
|
||||
(BuildContext context) => [
|
||||
PopupMenuItem<String>(
|
||||
value: 'toggle_status',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isActive ? Icons.toggle_off : Icons.toggle_on,
|
||||
color:
|
||||
isActive
|
||||
? Colors.red.shade700
|
||||
: Colors.green.shade700,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(isActive ? 'Nonaktifkan' : 'Aktifkan'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.edit, color: Colors.blue, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('Edit'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete, color: Colors.red, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('Hapus'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoRow(Icons.phone, 'Kontak', partner['contact']),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow(Icons.location_on, 'Alamat', partner['address']),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow(
|
||||
Icons.calendar_today,
|
||||
'Tanggal Bergabung',
|
||||
partner['join_date'],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(IconData icon, String label, String value) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$label:',
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey.shade700),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _handleMenuAction(String action, Map<String, dynamic> partner) {
|
||||
switch (action) {
|
||||
case 'toggle_status':
|
||||
controller.togglePartnerStatus(partner['id']);
|
||||
break;
|
||||
case 'edit':
|
||||
_showEditPartnerDialog(Get.context!, partner);
|
||||
break;
|
||||
case 'delete':
|
||||
_showDeleteConfirmationDialog(Get.context!, partner);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _showHelpDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Bantuan Petugas Mitra',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildHelpItem(
|
||||
Icons.add_circle_outline,
|
||||
'Tambah Mitra',
|
||||
'Tekan tombol + untuk menambah petugas mitra baru',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildHelpItem(
|
||||
Icons.toggle_on,
|
||||
'Aktif/Nonaktif',
|
||||
'Ubah status aktif petugas mitra melalui menu opsi',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildHelpItem(
|
||||
Icons.edit,
|
||||
'Edit Data',
|
||||
'Ubah informasi petugas mitra melalui menu opsi',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildHelpItem(
|
||||
Icons.delete,
|
||||
'Hapus',
|
||||
'Hapus petugas mitra melalui menu opsi',
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Mengerti'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHelpItem(IconData icon, String title, String description) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, color: AppColorsPetugas.blueGrotto, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddPartnerDialog(BuildContext context) {
|
||||
final nameController = TextEditingController();
|
||||
final contactController = TextEditingController();
|
||||
final addressController = TextEditingController();
|
||||
final roleController = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Tambah Petugas Mitra',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildTextField(nameController, 'Nama Lengkap', Icons.person),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
contactController,
|
||||
'Nomor Kontak',
|
||||
Icons.phone,
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
addressController,
|
||||
'Alamat',
|
||||
Icons.location_on,
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(roleController, 'Jabatan', Icons.work),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: AppColorsPetugas.navyBlue),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Batal',
|
||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (nameController.text.isEmpty ||
|
||||
contactController.text.isEmpty ||
|
||||
addressController.text.isEmpty ||
|
||||
roleController.text.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Harap isi semua data',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final newPartner = {
|
||||
'id':
|
||||
DateTime.now().millisecondsSinceEpoch
|
||||
.toString(),
|
||||
'name': nameController.text,
|
||||
'contact': contactController.text,
|
||||
'address': addressController.text,
|
||||
'role': roleController.text,
|
||||
'is_active': true,
|
||||
'join_date':
|
||||
'${DateTime.now().day} ${_getMonthName(DateTime.now().month)} ${DateTime.now().year}',
|
||||
};
|
||||
|
||||
controller.addPartner(newPartner);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditPartnerDialog(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> partner,
|
||||
) {
|
||||
final nameController = TextEditingController(text: partner['name']);
|
||||
final contactController = TextEditingController(text: partner['contact']);
|
||||
final addressController = TextEditingController(text: partner['address']);
|
||||
final roleController = TextEditingController(text: partner['role']);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Edit Petugas Mitra',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildTextField(nameController, 'Nama Lengkap', Icons.person),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
contactController,
|
||||
'Nomor Kontak',
|
||||
Icons.phone,
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
addressController,
|
||||
'Alamat',
|
||||
Icons.location_on,
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(roleController, 'Jabatan', Icons.work),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: AppColorsPetugas.navyBlue),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Batal',
|
||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (nameController.text.isEmpty ||
|
||||
contactController.text.isEmpty ||
|
||||
addressController.text.isEmpty ||
|
||||
roleController.text.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Harap isi semua data',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final updatedPartner = {
|
||||
'id': partner['id'],
|
||||
'name': nameController.text,
|
||||
'contact': contactController.text,
|
||||
'address': addressController.text,
|
||||
'role': roleController.text,
|
||||
'is_active': partner['is_active'],
|
||||
'join_date': partner['join_date'],
|
||||
};
|
||||
|
||||
controller.editPartner(
|
||||
partner['id'],
|
||||
updatedPartner,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmationDialog(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> partner,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Konfirmasi Penghapusan'),
|
||||
content: Text(
|
||||
'Apakah Anda yakin ingin menghapus petugas mitra "${partner['name']}"?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
controller.deletePartner(partner['id']);
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField(
|
||||
TextEditingController controller,
|
||||
String label,
|
||||
IconData icon, {
|
||||
TextInputType? keyboardType,
|
||||
int maxLines = 1,
|
||||
}) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
prefixIcon: Icon(icon),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getMonthName(int month) {
|
||||
const months = [
|
||||
'',
|
||||
'Januari',
|
||||
'Februari',
|
||||
'Maret',
|
||||
'April',
|
||||
'Mei',
|
||||
'Juni',
|
||||
'Juli',
|
||||
'Agustus',
|
||||
'September',
|
||||
'Oktober',
|
||||
'November',
|
||||
'Desember',
|
||||
];
|
||||
return months[month];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,691 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/list_tagihan_periode_controller.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class ListTagihanPeriodeView extends GetView<ListTagihanPeriodeController> {
|
||||
const ListTagihanPeriodeView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get dashboard controller for navigation
|
||||
final dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'Riwayat Tagihan',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
drawer: PetugasSideNavbar(controller: dashboardController),
|
||||
drawerEdgeDragWidth: 60,
|
||||
drawerScrimColor: Colors.black.withOpacity(0.6),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [_buildHeader(), Expanded(child: _buildPeriodeList())],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
offset: const Offset(0, 2),
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(() {
|
||||
final pelanggan = controller.pelangganData.value;
|
||||
final nama = pelanggan['nama'] ?? 'Pelanggan';
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: AppColorsPetugas.babyBlueBright,
|
||||
child: Text(
|
||||
nama.substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
nama,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Obx(
|
||||
() => Text(
|
||||
'Pelanggan ${controller.serviceName.value}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
size: 14,
|
||||
color: Colors.green,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Aktif',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Riwayat Tagihan Bulanan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Daftar periode tagihan dan status pembayaran',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPeriodeList() {
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.filteredPeriodeList.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredPeriodeList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final periode = controller.filteredPeriodeList[index];
|
||||
return _buildPeriodeCard(periode);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPeriodeCard(Map<String, dynamic> periode) {
|
||||
final statusColor = Color(
|
||||
controller.getStatusColor(periode['status_pembayaran']),
|
||||
);
|
||||
final isCurrent = periode['is_current'] ?? false;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
border:
|
||||
isCurrent
|
||||
? Border.all(color: AppColorsPetugas.blueGrotto, width: 2)
|
||||
: null,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Get.snackbar(
|
||||
'Informasi',
|
||||
'Detail tagihan untuk periode ini tidak tersedia',
|
||||
backgroundColor: Colors.orange.withOpacity(0.1),
|
||||
colorText: Colors.orange.shade800,
|
||||
duration: const Duration(seconds: 3),
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(8),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isCurrent
|
||||
? AppColorsPetugas.babyBlueBright.withOpacity(0.3)
|
||||
: Colors.white,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
periode['bulan'].substring(0, 3),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
periode['tahun'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Periode ${controller.getPeriodeString(periode)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Jatuh tempo: 20 ${periode['bulan']} ${periode['tahun']}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
periode['status_pembayaran'].toLowerCase() == 'lunas'
|
||||
? Icons.check_circle
|
||||
: Icons.pending,
|
||||
size: 14,
|
||||
color: statusColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
periode['status_pembayaran'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Nominal',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
periode['nominal'],
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (periode['status_pembayaran'].toLowerCase() == 'lunas')
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'Tanggal Bayar',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
periode['tanggal_pembayaran'],
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: Icon(
|
||||
Icons.payment,
|
||||
size: 16,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
label: Text(
|
||||
'Bayar Sekarang',
|
||||
style: TextStyle(color: AppColorsPetugas.blueGrotto),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: BorderSide(color: AppColorsPetugas.blueGrotto),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isCurrent)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright.withOpacity(0.3),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Periode Berjalan',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.receipt_long, size: 60, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Tidak ada riwayat tagihan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Pelanggan belum memiliki riwayat tagihan',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPeriodeDetails(Map<String, dynamic> periode) {
|
||||
final statusColor = Color(
|
||||
controller.getStatusColor(periode['status_pembayaran']),
|
||||
);
|
||||
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
periode['bulan'].substring(0, 3),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
periode['tahun'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Detail Tagihan',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Periode ${controller.getPeriodeString(periode)}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
periode['status_pembayaran'].toLowerCase() ==
|
||||
'lunas'
|
||||
? Icons.check_circle
|
||||
: Icons.pending,
|
||||
size: 14,
|
||||
color: statusColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
periode['status_pembayaran'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Informasi Tagihan',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildDetailRow(
|
||||
icon: Icons.person,
|
||||
label: 'Pelanggan',
|
||||
value:
|
||||
controller.pelangganData.value['nama'] ?? 'Pelanggan',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.calendar_today,
|
||||
label: 'Periode',
|
||||
value: controller.getPeriodeString(periode),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.attach_money,
|
||||
label: 'Nominal',
|
||||
value: periode['nominal'],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.event,
|
||||
label: 'Jatuh Tempo',
|
||||
value: '20 ${periode['bulan']} ${periode['tahun']}',
|
||||
),
|
||||
if (periode['status_pembayaran'].toLowerCase() ==
|
||||
'lunas') ...[
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Informasi Pembayaran',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildDetailRow(
|
||||
icon: Icons.date_range,
|
||||
label: 'Tanggal Pembayaran',
|
||||
value: periode['tanggal_pembayaran'],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.payment,
|
||||
label: 'Metode Pembayaran',
|
||||
value: periode['metode_pembayaran'],
|
||||
),
|
||||
if (periode['keterangan'] != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildDetailRow(
|
||||
icon: Icons.info_outline,
|
||||
label: 'Keterangan',
|
||||
value: periode['keterangan'],
|
||||
),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.close),
|
||||
label: const Text('Tutup'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, size: 16, color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
1291
lib/app/modules/petugas_bumdes/views/petugas_aset_view.dart
Normal file
1291
lib/app/modules/petugas_bumdes/views/petugas_aset_view.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,518 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_bumdes_cbp_controller.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class PetugasBumdesCbpView extends GetView<PetugasBumdesCbpController> {
|
||||
const PetugasBumdesCbpView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'BUMDes CBP',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
),
|
||||
drawer: _buildDrawer(),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Text
|
||||
const Text(
|
||||
'Pengelolaan BUMDes CBP',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF333333),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Kelola informasi akun bank dan petugas mitra BUMDes',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Main Content
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
// Bank Account Card
|
||||
_buildInfoCard(
|
||||
title: 'Rekening Bank',
|
||||
icon: Icons.account_balance_outlined,
|
||||
primaryInfo:
|
||||
'${controller.bankAccounts.length} Rekening Terdaftar',
|
||||
secondaryInfo:
|
||||
controller.bankAccounts.isNotEmpty
|
||||
? 'Rekening Utama: ${controller.bankAccounts.firstWhere((acc) => acc['is_primary'] == true, orElse: () => {'bank_name': 'Tidak ada'})['bank_name']}'
|
||||
: 'Belum ada rekening utama',
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF0072B5), Color(0xFF0088CC)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
onTap: _showBankAccountsPage,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Partners Card
|
||||
_buildInfoCard(
|
||||
title: 'Petugas Mitra',
|
||||
icon: Icons.people_outline_rounded,
|
||||
primaryInfo: '${controller.partners.length} Mitra',
|
||||
secondaryInfo:
|
||||
'${controller.partners.where((p) => p['is_active'] == true).length} Mitra Aktif',
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF00B4D8), Color(0xFF48CAE4)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
onTap: _showPartnersPage,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: _buildBottomNavigationBar(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoCard({
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required String primaryInfo,
|
||||
required String secondaryInfo,
|
||||
required Gradient gradient,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: gradient,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: gradient.colors.first.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: Colors.white, size: 30),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
primaryInfo,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
secondaryInfo,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.85),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Lihat Detail',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Icon(Icons.arrow_forward, color: Colors.white, size: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomNavigationBar() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(0),
|
||||
topRight: Radius.circular(0),
|
||||
),
|
||||
child: BottomNavigationBar(
|
||||
currentIndex: 5, // BUMDes tab
|
||||
type: BottomNavigationBarType.fixed,
|
||||
backgroundColor: Colors.white,
|
||||
selectedItemColor: AppColorsPetugas.blueGrotto,
|
||||
unselectedItemColor: Colors.grey,
|
||||
selectedLabelStyle: const TextStyle(fontSize: 12),
|
||||
unselectedLabelStyle: const TextStyle(fontSize: 12),
|
||||
onTap: (index) {
|
||||
// Use the dashboard controller to handle tab navigation
|
||||
// This is typically provided by the parent Dashboard
|
||||
final dashboardController =
|
||||
Get.find<PetugasBumdesDashboardController>();
|
||||
dashboardController.changeTab(index);
|
||||
},
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.dashboard_outlined),
|
||||
activeIcon: Icon(Icons.dashboard),
|
||||
label: 'Dashboard',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.inventory_2_outlined),
|
||||
activeIcon: Icon(Icons.inventory_2),
|
||||
label: 'Aset',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.archive_outlined),
|
||||
activeIcon: Icon(Icons.archive),
|
||||
label: 'Paket',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.assignment_outlined),
|
||||
activeIcon: Icon(Icons.assignment),
|
||||
label: 'Sewa',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.subscriptions_outlined),
|
||||
activeIcon: Icon(Icons.subscriptions),
|
||||
label: 'Langganan',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.business_outlined),
|
||||
activeIcon: Icon(Icons.business),
|
||||
label: 'BUMDes',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDrawer() {
|
||||
return Drawer(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
DrawerHeader(
|
||||
decoration: BoxDecoration(color: AppColorsPetugas.navyBlue),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
backgroundColor: Colors.white,
|
||||
radius: 30,
|
||||
child: Icon(Icons.person, size: 40, color: Colors.blueGrey),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Admin BUMDes',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'admin@bumdes.desa.id',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.dashboard_outlined),
|
||||
title: const Text('Dashboard'),
|
||||
onTap: () {
|
||||
Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.inventory_2_outlined),
|
||||
title: const Text('Kelola Aset'),
|
||||
onTap: () {
|
||||
Get.offAllNamed(Routes.PETUGAS_ASET);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.feed_outlined),
|
||||
title: const Text('Kelola Paket'),
|
||||
onTap: () {
|
||||
Get.offAllNamed(Routes.PETUGAS_PAKET);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.assignment_outlined),
|
||||
title: const Text('Kelola Permintaan Sewa'),
|
||||
onTap: () {
|
||||
Get.offAllNamed(Routes.PETUGAS_SEWA);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.subscriptions_outlined),
|
||||
title: const Text('Kelola Langganan'),
|
||||
onTap: () {
|
||||
Get.offAllNamed(Routes.PETUGAS_LANGGANAN);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.business_outlined),
|
||||
title: const Text('BUMDes CBP'),
|
||||
tileColor: Colors.blue.shade50,
|
||||
onTap: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: const Text('Logout'),
|
||||
onTap: () {
|
||||
// Implement logout
|
||||
Get.offAllNamed(Routes.LOGIN);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Method to handle navigation to bank accounts management
|
||||
void _showBankAccountsPage() {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.account_balance,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text(
|
||||
'Rekening Bank',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Obx(
|
||||
() =>
|
||||
controller.bankAccounts.isEmpty
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Text(
|
||||
'Belum ada rekening yang terdaftar',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
children:
|
||||
controller.bankAccounts
|
||||
.map(
|
||||
(account) => _buildBankAccountItem(account),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
// Show the full-screen bank accounts page
|
||||
Get.snackbar(
|
||||
'Informasi',
|
||||
'Menuju halaman kelola rekening bank',
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
label: const Text('Lihat Semua Rekening'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBankAccountItem(Map<String, dynamic> account) {
|
||||
final isPrimary = account['is_primary'] as bool;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isPrimary ? AppColorsPetugas.blueGrotto : Colors.grey.shade300,
|
||||
width: isPrimary ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.credit_card,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
account['bank_name'],
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (isPrimary) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.blueGrotto.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'Utama',
|
||||
style: TextStyle(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
account['account_number'],
|
||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Method to handle navigation to partners management
|
||||
void _showPartnersPage() {
|
||||
// Navigate to the ListPetugasMitraView
|
||||
Get.toNamed(Routes.LIST_PETUGAS_MITRA);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
2054
lib/app/modules/petugas_bumdes/views/petugas_detail_sewa_view.dart
Normal file
2054
lib/app/modules/petugas_bumdes/views/petugas_detail_sewa_view.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
||||
|
||||
@ -0,0 +1,914 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_manajemen_bumdes_controller.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
|
||||
class PetugasManajemenBumdesView
|
||||
extends GetView<PetugasManajemenBumdesController> {
|
||||
const PetugasManajemenBumdesView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Manajemen BUMDes'),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: Obx(
|
||||
() =>
|
||||
controller.isLoading.value
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Column(
|
||||
children: [
|
||||
_buildTabBar(),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
switch (controller.selectedTabIndex.value) {
|
||||
case 0:
|
||||
return _buildProfileTab();
|
||||
case 1:
|
||||
return _buildBankAccountTab();
|
||||
case 2:
|
||||
return _buildPartnerTab();
|
||||
default:
|
||||
return _buildProfileTab();
|
||||
}
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: _buildBottomNav(dashboardController),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color(0x29000000),
|
||||
offset: Offset(0, 3),
|
||||
blurRadius: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildTab(0, 'Profile'),
|
||||
_buildTab(1, 'Rekening Bank'),
|
||||
_buildTab(2, 'Mitra'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTab(int index, String title) {
|
||||
final isSelected = controller.selectedTabIndex.value == index;
|
||||
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.changeTab(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color:
|
||||
isSelected ? AppColorsPetugas.navyBlue : Colors.transparent,
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color:
|
||||
isSelected ? AppColorsPetugas.navyBlue : Colors.grey.shade600,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileTab() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Profile BUMDes',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildProfileForm(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileForm() {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildProfileField('Nama BUMDes', 'BUMDes Sejahtera'),
|
||||
_buildProfileField('Alamat', 'Jl. Desa No. 123, Kecamatan Makmur'),
|
||||
_buildProfileField('Email', 'bumdes.sejahtera@gmail.com'),
|
||||
_buildProfileField('Telepon', '081234567890'),
|
||||
_buildProfileField(
|
||||
'Deskripsi',
|
||||
'BUMDes yang bergerak dalam bidang penyewaan aset dan paket untuk masyarakat desa.',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
minimumSize: const Size(double.infinity, 45),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Edit Profile'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileField(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: const TextStyle(fontSize: 14)),
|
||||
const Divider(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBankAccountTab() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Rekening Bank',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showAddBankAccountDialog(),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Tambah'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.bankAccounts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final account = controller.bankAccounts[index];
|
||||
return _buildBankAccountCard(account, index);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBankAccountCard(Map<String, dynamic> account, int index) {
|
||||
final isPrimary = account['isPrimary'] as bool;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(
|
||||
color: isPrimary ? AppColorsPetugas.navyBlue : Colors.transparent,
|
||||
width: isPrimary ? 2 : 0,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
account['bankName'],
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
if (!isPrimary)
|
||||
PopupMenuItem(
|
||||
value: 'primary',
|
||||
child: const Text('Jadikan Utama'),
|
||||
),
|
||||
const PopupMenuItem(value: 'edit', child: Text('Edit')),
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text('Hapus'),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'primary':
|
||||
controller.setPrimaryBankAccount(index);
|
||||
break;
|
||||
case 'edit':
|
||||
_showEditBankAccountDialog(account, index);
|
||||
break;
|
||||
case 'delete':
|
||||
_showDeleteBankAccountDialog(index);
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildBankAccountInfo('Nama Pemilik', account['accountName']),
|
||||
_buildBankAccountInfo('Nomor Rekening', account['accountNumber']),
|
||||
if (isPrimary)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.navyBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'Rekening Utama',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBankAccountInfo(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
Expanded(child: Text(value, style: const TextStyle(fontSize: 14))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPartnerTab() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Mitra BUMDes',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showAddPartnerDialog(),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Tambah'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.partners.length,
|
||||
itemBuilder: (context, index) {
|
||||
final partner = controller.partners[index];
|
||||
return _buildPartnerCard(partner, index);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPartnerCard(Map<String, dynamic> partner, int index) {
|
||||
final isActive = partner['isActive'] as bool;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
partner['name'],
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Switch(
|
||||
value: isActive,
|
||||
onChanged:
|
||||
(value) => controller.togglePartnerStatus(index),
|
||||
activeColor: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Text('Edit'),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text('Hapus'),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'edit':
|
||||
_showEditPartnerDialog(partner, index);
|
||||
break;
|
||||
case 'delete':
|
||||
_showDeletePartnerDialog(index);
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildPartnerInfo('Email', partner['email']),
|
||||
_buildPartnerInfo('Telepon', partner['phone']),
|
||||
_buildPartnerInfo('Alamat', partner['address']),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isActive
|
||||
? Colors.green.withOpacity(0.1)
|
||||
: Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
isActive ? 'Aktif' : 'Tidak Aktif',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isActive ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPartnerInfo(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
Expanded(child: Text(value, style: const TextStyle(fontSize: 14))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomNav(PetugasBumdesDashboardController dashboardController) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildNavItem(Icons.dashboard, 'Dashboard', 0, dashboardController),
|
||||
_buildNavItem(Icons.inventory, 'Aset', 1, dashboardController),
|
||||
_buildNavItem(Icons.inventory_2, 'Paket', 2, dashboardController),
|
||||
_buildNavItem(Icons.shopping_cart, 'Sewa', 3, dashboardController),
|
||||
_buildNavItem(
|
||||
Icons.subscriptions,
|
||||
'Langganan',
|
||||
4,
|
||||
dashboardController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem(
|
||||
IconData icon,
|
||||
String label,
|
||||
int index,
|
||||
PetugasBumdesDashboardController dashboardController,
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: () => dashboardController.changeTab(index),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: AppColorsPetugas.blueGrotto, size: 24),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 12, color: AppColorsPetugas.blueGrotto),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddBankAccountDialog() {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Tambah Rekening Bank',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nama Bank',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nama Pemilik Rekening',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nomor Rekening',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur tambah rekening bank sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditBankAccountDialog(Map<String, dynamic> account, int index) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Edit Rekening Bank',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nama Bank',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: account['bankName']),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nama Pemilik Rekening',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: account['accountName']),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nomor Rekening',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(
|
||||
text: account['accountNumber'],
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur edit rekening bank sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteBankAccountDialog(int index) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Hapus Rekening Bank',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Apakah Anda yakin ingin menghapus rekening bank ini?',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur hapus rekening bank sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddPartnerDialog() {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Tambah Mitra',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nama Mitra',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Telepon',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Alamat',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur tambah mitra sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditPartnerDialog(Map<String, dynamic> partner, int index) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Edit Mitra',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nama Mitra',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: partner['name']),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: partner['email']),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Telepon',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: partner['phone']),
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Alamat',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: partner['address']),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur edit mitra sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeletePartnerDialog(int index) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Hapus Mitra',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Apakah Anda yakin ingin menghapus mitra ini?',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur hapus mitra sedang dalam pengembangan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
700
lib/app/modules/petugas_bumdes/views/petugas_paket_view.dart
Normal file
700
lib/app/modules/petugas_bumdes/views/petugas_paket_view.dart
Normal file
@ -0,0 +1,700 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_paket_controller.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
const PetugasPaketView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get dashboard controller for navigation
|
||||
final dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
// Saat back button ditekan, kembali ke dashboard
|
||||
dashboardController.changeTab(0);
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Manajemen Paket',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sort, size: 22),
|
||||
onPressed: () => _showSortingBottomSheet(context),
|
||||
tooltip: 'Urutkan',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
drawer: PetugasSideNavbar(controller: dashboardController),
|
||||
drawerEdgeDragWidth: 60,
|
||||
drawerScrimColor: Colors.black.withOpacity(0.6),
|
||||
backgroundColor: AppColorsPetugas.babyBlueBright,
|
||||
body: Column(
|
||||
children: [_buildSearchBar(), Expanded(child: _buildPaketList())],
|
||||
),
|
||||
bottomNavigationBar: Obx(
|
||||
() => PetugasBumdesBottomNavbar(
|
||||
selectedIndex: dashboardController.currentTabIndex.value,
|
||||
onItemTapped: (index) => dashboardController.changeTab(index),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET),
|
||||
label: Text(
|
||||
'Tambah Paket',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
icon: Icon(Icons.add, color: AppColorsPetugas.blueGrotto),
|
||||
backgroundColor: AppColorsPetugas.babyBlueBright,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColorsPetugas.shadowColor,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
onChanged: controller.setSearchQuery,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari paket...',
|
||||
hintStyle: TextStyle(color: Colors.grey.shade400),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColorsPetugas.babyBlueBright,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaketList() {
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.filteredPaketList.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.category_outlined,
|
||||
size: 80,
|
||||
color: AppColorsPetugas.babyBlue,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Tidak ada paket ditemukan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Tambah Paket'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: controller.loadPaketData,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredPaketList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final paket = controller.filteredPaketList[index];
|
||||
return _buildPaketCard(context, paket);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPaketCard(BuildContext context, Map<String, dynamic> paket) {
|
||||
final isAvailable = paket['tersedia'] == true;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColorsPetugas.shadowColor,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _showPaketDetails(context, paket),
|
||||
child: Row(
|
||||
children: [
|
||||
// Paket image or icon
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
bottomLeft: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getPaketIcon(paket['kategori']),
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Paket info
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Name and price
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
paket['nama'],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Rp ${_formatPrice(paket['harga'])}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Status badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isAvailable
|
||||
? AppColorsPetugas.successLight
|
||||
: AppColorsPetugas.errorLight,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color:
|
||||
isAvailable
|
||||
? AppColorsPetugas.success
|
||||
: AppColorsPetugas.error,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
isAvailable ? 'Aktif' : 'Nonaktif',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color:
|
||||
isAvailable
|
||||
? AppColorsPetugas.success
|
||||
: AppColorsPetugas.error,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Action icons
|
||||
const SizedBox(width: 12),
|
||||
Row(
|
||||
children: [
|
||||
// Edit icon
|
||||
GestureDetector(
|
||||
onTap:
|
||||
() => _showAddEditPaketDialog(
|
||||
context,
|
||||
paket: paket,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: AppColorsPetugas.blueGrotto
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.edit_outlined,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Delete icon
|
||||
GestureDetector(
|
||||
onTap:
|
||||
() => _showDeleteConfirmation(context, paket),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.errorLight,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: AppColorsPetugas.error.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.delete_outline,
|
||||
color: AppColorsPetugas.error,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPrice(dynamic price) {
|
||||
if (price == null) return '0';
|
||||
|
||||
// Convert the price to string and handle formatting
|
||||
String priceStr = price.toString();
|
||||
|
||||
// Add thousand separators
|
||||
final RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))');
|
||||
String formatted = priceStr.replaceAllMapped(reg, (Match m) => '${m[1]}.');
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
IconData _getPaketIcon(String? category) {
|
||||
if (category == null) return Icons.category;
|
||||
|
||||
switch (category.toLowerCase()) {
|
||||
case 'bulanan':
|
||||
return Icons.calendar_month;
|
||||
case 'tahunan':
|
||||
return Icons.calendar_today;
|
||||
case 'premium':
|
||||
return Icons.star;
|
||||
case 'bisnis':
|
||||
return Icons.business;
|
||||
default:
|
||||
return Icons.category;
|
||||
}
|
||||
}
|
||||
|
||||
void _showSortingBottomSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Urutkan Paket',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...controller.sortOptions.map((option) {
|
||||
return Obx(() {
|
||||
final isSelected = option == controller.sortBy.value;
|
||||
return RadioListTile<String>(
|
||||
title: Text(option),
|
||||
value: option,
|
||||
groupValue: controller.sortBy.value,
|
||||
activeColor: AppColorsPetugas.blueGrotto,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
controller.setSortBy(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showPaketDetails(BuildContext context, Map<String, dynamic> paket) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.75,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
paket['nama'],
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, color: AppColorsPetugas.blueGrotto),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDetailItem('Kategori', paket['kategori']),
|
||||
_buildDetailItem(
|
||||
'Harga',
|
||||
controller.formatPrice(paket['harga']),
|
||||
),
|
||||
_buildDetailItem(
|
||||
'Status',
|
||||
paket['tersedia'] ? 'Tersedia' : 'Tidak Tersedia',
|
||||
),
|
||||
_buildDetailItem('Deskripsi', paket['deskripsi']),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Item dalam Paket',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListView.separated(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: paket['items'].length,
|
||||
separatorBuilder:
|
||||
(context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final item = paket['items'][index];
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColorsPetugas.babyBlue,
|
||||
child: Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
title: Text(item['nama']),
|
||||
trailing: Text(
|
||||
'${item['jumlah']} unit',
|
||||
style: TextStyle(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_PAKET,
|
||||
arguments: paket,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.edit),
|
||||
label: const Text('Edit'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColorsPetugas.blueGrotto,
|
||||
side: BorderSide(color: AppColorsPetugas.blueGrotto),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_showDeleteConfirmation(context, paket);
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
label: const Text('Hapus'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.error,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailItem(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 14, color: AppColorsPetugas.blueGrotto),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddEditPaketDialog(
|
||||
BuildContext context, {
|
||||
Map<String, dynamic>? paket,
|
||||
}) {
|
||||
final isEditing = paket != null;
|
||||
|
||||
// This would be implemented with proper form validation in a real app
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
isEditing ? 'Edit Paket' : 'Tambah Paket Baru',
|
||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
content: const Text(
|
||||
'Form pengelolaan paket akan ditampilkan di sini dengan field untuk nama, kategori, harga, deskripsi, status, dan item-item dalam paket.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Batal',
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
// In a real app, we would save the form data
|
||||
Get.snackbar(
|
||||
isEditing ? 'Paket Diperbarui' : 'Paket Ditambahkan',
|
||||
isEditing
|
||||
? 'Paket berhasil diperbarui'
|
||||
: 'Paket baru berhasil ditambahkan',
|
||||
backgroundColor: AppColorsPetugas.success,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
child: Text(isEditing ? 'Simpan' : 'Tambah'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmation(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> paket,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'Konfirmasi Hapus',
|
||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
content: Text(
|
||||
'Apakah Anda yakin ingin menghapus paket "${paket['nama']}"?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Batal',
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
controller.deletePaket(paket['id']);
|
||||
Get.snackbar(
|
||||
'Paket Dihapus',
|
||||
'Paket berhasil dihapus dari sistem',
|
||||
backgroundColor: AppColorsPetugas.error,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.error,
|
||||
),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
682
lib/app/modules/petugas_bumdes/views/petugas_sewa_view.dart
Normal file
682
lib/app/modules/petugas_bumdes/views/petugas_sewa_view.dart
Normal file
@ -0,0 +1,682 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_sewa_controller.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import 'petugas_detail_sewa_view.dart';
|
||||
|
||||
class PetugasSewaView extends StatefulWidget {
|
||||
const PetugasSewaView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PetugasSewaView> createState() => _PetugasSewaViewState();
|
||||
}
|
||||
|
||||
class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
late PetugasSewaController controller;
|
||||
late PetugasBumdesDashboardController dashboardController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = Get.find<PetugasSewaController>();
|
||||
dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||
|
||||
_tabController = TabController(
|
||||
length: controller.statusFilters.length,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// Add listener to sync tab selection with controller's filter
|
||||
_tabController.addListener(_onTabChanged);
|
||||
|
||||
// Listen to controller's filter changes
|
||||
ever(controller.selectedStatusFilter, _onFilterChanged);
|
||||
}
|
||||
|
||||
void _onTabChanged() {
|
||||
if (!_tabController.indexIsChanging) {
|
||||
final selectedStatus = controller.statusFilters[_tabController.index];
|
||||
controller.setStatusFilter(selectedStatus);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFilterChanged(String status) {
|
||||
final index = controller.statusFilters.indexOf(status);
|
||||
if (index != -1 && index != _tabController.index) {
|
||||
_tabController.animateTo(index);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.removeListener(_onTabChanged);
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
dashboardController.changeTab(0);
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Manajemen Sewa',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.filter_alt_outlined, size: 22),
|
||||
onPressed: () => _showFilterBottomSheet(),
|
||||
tooltip: 'Filter',
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(60),
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
indicator: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicatorPadding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 4,
|
||||
),
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.white.withOpacity(0.7),
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
tabs:
|
||||
controller.statusFilters
|
||||
.map(
|
||||
(status) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Tab(text: status),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
dividerColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
drawer: PetugasSideNavbar(controller: dashboardController),
|
||||
drawerEdgeDragWidth: 60,
|
||||
drawerScrimColor: Colors.black.withOpacity(0.6),
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildSearchSection(),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children:
|
||||
controller.statusFilters.map((status) {
|
||||
return _buildSewaListForStatus(status);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: Obx(
|
||||
() => PetugasBumdesBottomNavbar(
|
||||
selectedIndex: dashboardController.currentTabIndex.value,
|
||||
onItemTapped: (index) => dashboardController.changeTab(index),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
onChanged: (value) {
|
||||
controller.setSearchQuery(value);
|
||||
controller.setOrderIdQuery(value);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari nama warga atau ID pesanan...',
|
||||
hintStyle: TextStyle(color: Colors.grey.shade400, fontSize: 14),
|
||||
prefixIcon: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Icon(
|
||||
Icons.search_rounded,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true,
|
||||
suffixIcon: Icon(
|
||||
Icons.tune_rounded,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSewaListForStatus(String status) {
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Memuat data...',
|
||||
style: TextStyle(
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final filteredList =
|
||||
status == 'Semua'
|
||||
? controller.filteredSewaList
|
||||
: status == 'Periksa Pembayaran'
|
||||
? controller.sewaList
|
||||
.where(
|
||||
(sewa) =>
|
||||
sewa['status'] == 'Periksa Pembayaran' ||
|
||||
sewa['status'] == 'Pembayaran Denda' ||
|
||||
sewa['status'] == 'Periksa Denda',
|
||||
)
|
||||
.toList()
|
||||
: controller.sewaList
|
||||
.where((sewa) => sewa['status'] == status)
|
||||
.toList();
|
||||
|
||||
if (filteredList.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 70,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Tidak ada sewa ditemukan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
status == 'Semua'
|
||||
? 'Belum ada data sewa untuk kriteria yang dipilih'
|
||||
: status == 'Periksa Pembayaran'
|
||||
? 'Belum ada data sewa yang perlu pembayaran diverifikasi atau memiliki denda'
|
||||
: 'Belum ada data sewa dengan status "$status"',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: controller.loadSewaData,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: filteredList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final sewa = filteredList[index];
|
||||
return _buildSewaCard(context, sewa);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildSewaCard(BuildContext context, Map<String, dynamic> sewa) {
|
||||
final statusColor = controller.getStatusColor(sewa['status']);
|
||||
final status = sewa['status'];
|
||||
|
||||
// Get appropriate icon for status
|
||||
IconData statusIcon;
|
||||
switch (status) {
|
||||
case 'Menunggu Pembayaran':
|
||||
statusIcon = Icons.payments_outlined;
|
||||
break;
|
||||
case 'Periksa Pembayaran':
|
||||
statusIcon = Icons.fact_check_outlined;
|
||||
break;
|
||||
case 'Diterima':
|
||||
statusIcon = Icons.check_circle_outlined;
|
||||
break;
|
||||
case 'Pembayaran Denda':
|
||||
statusIcon = Icons.money_off_csred_outlined;
|
||||
break;
|
||||
case 'Periksa Denda':
|
||||
statusIcon = Icons.assignment_late_outlined;
|
||||
break;
|
||||
case 'Dikembalikan':
|
||||
statusIcon = Icons.assignment_return_outlined;
|
||||
break;
|
||||
case 'Selesai':
|
||||
statusIcon = Icons.task_alt_outlined;
|
||||
break;
|
||||
case 'Dibatalkan':
|
||||
statusIcon = Icons.cancel_outlined;
|
||||
break;
|
||||
default:
|
||||
statusIcon = Icons.help_outline_rounded;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: InkWell(
|
||||
onTap: () => Get.to(() => PetugasDetailSewaView(sewa: sewa)),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Customer Circle Avatar
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||
child: Text(
|
||||
sewa['nama_warga'].substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Customer details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sewa['nama_warga'],
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
statusIcon,
|
||||
size: 12,
|
||||
color: statusColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'#${sewa['order_id']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Price
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.blueGrotto.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
controller.formatPrice(sewa['total_biaya']),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Divider
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Divider(height: 1, color: Colors.grey.shade200),
|
||||
),
|
||||
|
||||
// Asset details
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
// Asset icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 20,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Asset name and duration
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sewa['nama_aset'],
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today_rounded,
|
||||
size: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${sewa['tanggal_mulai']} - ${sewa['tanggal_selesai']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Chevron icon
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFilterBottomSheet() {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Filter',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Status',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Obx(() {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
children:
|
||||
controller.statusFilters.map((status) {
|
||||
final isSelected =
|
||||
status == controller.selectedStatusFilter.value;
|
||||
return ChoiceChip(
|
||||
label: Text(status),
|
||||
selected: isSelected,
|
||||
selectedColor: AppColorsPetugas.blueGrotto,
|
||||
backgroundColor: Colors.white,
|
||||
labelStyle: TextStyle(
|
||||
color:
|
||||
isSelected
|
||||
? Colors.white
|
||||
: AppColorsPetugas.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: BorderSide(
|
||||
color:
|
||||
isSelected
|
||||
? AppColorsPetugas.blueGrotto
|
||||
: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
controller.setStatusFilter(status);
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
controller.resetFilters();
|
||||
Get.back();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: AppColorsPetugas.blueGrotto),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Reset',
|
||||
style: TextStyle(color: AppColorsPetugas.blueGrotto),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
controller.applyFilters();
|
||||
Get.back();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Terapkan',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
isDismissible: true,
|
||||
enableDrag: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,871 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../controllers/petugas_tambah_aset_controller.dart';
|
||||
|
||||
class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
const PetugasTambahAsetView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Tambah Aset',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [_buildHeaderSection(), _buildFormSection(context)],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: _buildBottomBar(),
|
||||
);
|
||||
}
|
||||
|
||||
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.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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormSection(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Basic Information Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.info_outline,
|
||||
title: 'Informasi Dasar',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
label: 'Nama Aset',
|
||||
hint: 'Masukkan nama aset',
|
||||
controller: controller.nameController,
|
||||
isRequired: true,
|
||||
prefixIcon: Icons.title,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
label: 'Deskripsi',
|
||||
hint: 'Masukkan deskripsi aset',
|
||||
controller: controller.descriptionController,
|
||||
maxLines: 3,
|
||||
isRequired: true,
|
||||
prefixIcon: Icons.description,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Media Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.photo_library,
|
||||
title: 'Media & Gambar',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildImageUploader(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Category Section
|
||||
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Category and Status as cards
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Kategori',
|
||||
options: controller.categoryOptions,
|
||||
selectedOption: controller.selectedCategory,
|
||||
onChanged: controller.setCategory,
|
||||
icon: Icons.inventory_2,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Status',
|
||||
options: controller.statusOptions,
|
||||
selectedOption: controller.selectedStatus,
|
||||
onChanged: controller.setStatus,
|
||||
icon: Icons.check_circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Quantity Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.format_list_numbered,
|
||||
title: 'Kuantitas & Pengukuran',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quantity fields
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildTextField(
|
||||
label: 'Kuantitas',
|
||||
hint: 'Jumlah aset',
|
||||
controller: controller.quantityController,
|
||||
isRequired: true,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
prefixIcon: Icons.numbers,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: _buildTextField(
|
||||
label: 'Satuan Ukur',
|
||||
hint: 'contoh: Unit, Buah',
|
||||
controller: controller.unitOfMeasureController,
|
||||
prefixIcon: Icons.straighten,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Rental Options Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.schedule,
|
||||
title: 'Opsi Waktu & Harga Sewa',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Time Options as cards
|
||||
_buildTimeOptionsCards(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Rental price fields based on selection
|
||||
Obx(
|
||||
() => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Per Hour Option
|
||||
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),
|
||||
|
||||
// Per Day Option
|
||||
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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 aset dengan basis perhitungan per jam'
|
||||
: 'Sewa aset 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader({required IconData icon, required String title}) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.blueGrotto.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: AppColorsPetugas.blueGrotto, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField({
|
||||
required String label,
|
||||
required String hint,
|
||||
required TextEditingController controller,
|
||||
bool isRequired = false,
|
||||
int maxLines = 1,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
String? prefixText,
|
||||
IconData? prefixIcon,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
if (isRequired) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'*',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
maxLines: maxLines,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
|
||||
filled: true,
|
||||
fillColor: AppColorsPetugas.babyBlueBright,
|
||||
prefixText: prefixText,
|
||||
prefixIcon:
|
||||
prefixIcon != null
|
||||
? Icon(
|
||||
prefixIcon,
|
||||
size: 20,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategorySelect({
|
||||
required String title,
|
||||
required List<String> options,
|
||||
required RxString selectedOption,
|
||||
required Function(String) onChanged,
|
||||
required IconData icon,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Obx(
|
||||
() => DropdownButtonFormField<String>(
|
||||
value: selectedOption.value,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(
|
||||
icon,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 20,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColorsPetugas.babyBlueBright,
|
||||
),
|
||||
items:
|
||||
options.map((option) {
|
||||
return DropdownMenuItem(
|
||||
value: option,
|
||||
child: Text(
|
||||
option,
|
||||
style: TextStyle(
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) onChanged(value);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
dropdownColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageUploader() {
|
||||
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: [
|
||||
Text(
|
||||
'Unggah Foto Aset',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Tambahkan foto aset untuk informasi visual.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Obx(
|
||||
() => Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
// Add button
|
||||
GestureDetector(
|
||||
onTap: () => controller.addSampleImage(),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: AppColorsPetugas.babyBlue,
|
||||
width: 1,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_photo_alternate_outlined,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tambah Foto',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Image previews
|
||||
...controller.selectedImages.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.removeImage(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: AppColorsPetugas.error,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
label: const Text('Batal'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColorsPetugas.textSecondary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
side: BorderSide(color: AppColorsPetugas.divider),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
final isValid = controller.isFormValid.value;
|
||||
final isSubmitting = controller.isSubmitting.value;
|
||||
return ElevatedButton.icon(
|
||||
onPressed:
|
||||
isValid && !isSubmitting ? controller.saveAsset : null,
|
||||
icon:
|
||||
isSubmitting
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.save),
|
||||
label: Text(isSubmitting ? 'Menyimpan...' : 'Simpan Aset'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
disabledBackgroundColor: AppColorsPetugas.textLight,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,932 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../controllers/petugas_tambah_paket_controller.dart';
|
||||
|
||||
class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
const PetugasTambahPaketView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Tambah Paket',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [_buildHeaderSection(), _buildFormSection(context)],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: _buildBottomBar(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.category,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Informasi Paket Baru',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Isi data dengan lengkap untuk menambahkan paket',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormSection(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Basic Information Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.info_outline,
|
||||
title: 'Informasi Dasar',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
label: 'Nama Paket',
|
||||
hint: 'Masukkan nama paket',
|
||||
controller: controller.nameController,
|
||||
isRequired: true,
|
||||
prefixIcon: Icons.title,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
label: 'Deskripsi',
|
||||
hint: 'Masukkan deskripsi paket',
|
||||
controller: controller.descriptionController,
|
||||
maxLines: 3,
|
||||
isRequired: true,
|
||||
prefixIcon: Icons.description,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Media Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.photo_library,
|
||||
title: 'Media & Gambar',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildImageUploader(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Category Section
|
||||
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Category and Status as cards
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Kategori',
|
||||
options: controller.categoryOptions,
|
||||
selectedOption: controller.selectedCategory,
|
||||
onChanged: controller.setCategory,
|
||||
icon: Icons.category,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Status',
|
||||
options: controller.statusOptions,
|
||||
selectedOption: controller.selectedStatus,
|
||||
onChanged: controller.setStatus,
|
||||
icon: Icons.check_circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Price Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.monetization_on,
|
||||
title: 'Harga Paket',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
label: 'Harga Paket',
|
||||
hint: 'Masukkan harga paket',
|
||||
controller: controller.priceController,
|
||||
isRequired: true,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
prefixText: 'Rp ',
|
||||
prefixIcon: Icons.payments,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Package Items Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.inventory_2,
|
||||
title: 'Item dalam Paket',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildPackageItems(),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPackageItems() {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Item Paket',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showAddItemDialog(),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Tambah Item'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||
foregroundColor: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Obx(
|
||||
() =>
|
||||
controller.packageItems.isEmpty
|
||||
? const Center(
|
||||
child: Text(
|
||||
'Belum ada item dalam paket',
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: controller.packageItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = controller.packageItems[index];
|
||||
return Card(
|
||||
elevation: 1,
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
title: Text(item['nama'] ?? 'Item Paket'),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Jumlah: ${item['jumlah']}'),
|
||||
if (item['stok'] != null)
|
||||
Text('Stok tersedia: ${item['stok']}'),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.edit,
|
||||
color: Colors.blue,
|
||||
),
|
||||
onPressed: () => _showEditItemDialog(index),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.delete,
|
||||
color: Colors.red,
|
||||
),
|
||||
onPressed:
|
||||
() => controller.removeItem(index),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddItemDialog() {
|
||||
// Reset controllers
|
||||
controller.selectedAsset.value = null;
|
||||
controller.itemQuantityController.clear();
|
||||
|
||||
// Fetch available assets
|
||||
controller.fetchAvailableAssets();
|
||||
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
if (controller.isLoadingAssets.value) {
|
||||
return const SizedBox(
|
||||
height: 150,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'Tambah Item ke Paket',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Asset dropdown
|
||||
DropdownButtonFormField<int>(
|
||||
value: controller.selectedAsset.value,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pilih Aset',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
hint: const Text('Pilih Aset'),
|
||||
items:
|
||||
controller.availableAssets.map((asset) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: asset['id'] as int,
|
||||
child: Text(
|
||||
'${asset['nama']} (Stok: ${asset['stok']})',
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
controller.setSelectedAsset(value);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quantity field
|
||||
Obx(() {
|
||||
// Calculate max quantity based on selected asset
|
||||
String? helperText;
|
||||
if (controller.selectedAsset.value != null) {
|
||||
final remaining = controller.getRemainingStock(
|
||||
controller.selectedAsset.value!,
|
||||
);
|
||||
helperText = 'Maksimal: $remaining unit';
|
||||
}
|
||||
|
||||
return TextFormField(
|
||||
controller: controller.itemQuantityController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Jumlah',
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: helperText,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
);
|
||||
}),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
controller.addAssetToPackage();
|
||||
Get.back();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||
foregroundColor: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
child: const Text('Tambah'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditItemDialog(int index) {
|
||||
final item = controller.packageItems[index];
|
||||
|
||||
// Set controllers
|
||||
controller.selectedAsset.value = item['asetId'];
|
||||
controller.itemQuantityController.text = item['jumlah'].toString();
|
||||
|
||||
// Fetch available assets
|
||||
controller.fetchAvailableAssets();
|
||||
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
if (controller.isLoadingAssets.value) {
|
||||
return const SizedBox(
|
||||
height: 150,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'Edit Item Paket',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Asset dropdown
|
||||
DropdownButtonFormField<int>(
|
||||
value: controller.selectedAsset.value,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pilih Aset',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
hint: const Text('Pilih Aset'),
|
||||
items:
|
||||
controller.availableAssets.map((asset) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: asset['id'] as int,
|
||||
child: Text(
|
||||
'${asset['nama']} (Stok: ${asset['stok']})',
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
controller.setSelectedAsset(value);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quantity field
|
||||
Obx(() {
|
||||
// Calculate max quantity based on selected asset
|
||||
String? helperText;
|
||||
if (controller.selectedAsset.value != null) {
|
||||
// Get the appropriate max quantity for editing
|
||||
final currentItem = controller.packageItems[index];
|
||||
final isCurrentAsset =
|
||||
currentItem['asetId'] == controller.selectedAsset.value;
|
||||
|
||||
int maxQuantity;
|
||||
if (isCurrentAsset) {
|
||||
// For same asset, include current quantity in calculation
|
||||
final asset = controller.availableAssets.firstWhere(
|
||||
(a) => a['id'] == controller.selectedAsset.value,
|
||||
orElse: () => {'stok': 0},
|
||||
);
|
||||
|
||||
final totalUsed = controller.packageItems
|
||||
.where(
|
||||
(item) =>
|
||||
item['asetId'] ==
|
||||
controller.selectedAsset.value &&
|
||||
controller.packageItems.indexOf(item) != index,
|
||||
)
|
||||
.fold(
|
||||
0,
|
||||
(sum, item) => sum + (item['jumlah'] as int),
|
||||
);
|
||||
|
||||
maxQuantity = (asset['stok'] as int) - totalUsed;
|
||||
} else {
|
||||
// For different asset, use remaining stock
|
||||
maxQuantity = controller.getRemainingStock(
|
||||
controller.selectedAsset.value!,
|
||||
);
|
||||
}
|
||||
|
||||
helperText = 'Maksimal: $maxQuantity unit';
|
||||
}
|
||||
|
||||
return TextFormField(
|
||||
controller: controller.itemQuantityController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Jumlah',
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: helperText,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
);
|
||||
}),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
controller.updatePackageItem(index);
|
||||
Get.back();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||
foregroundColor: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader({required IconData icon, required String title}) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.blueGrotto.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: AppColorsPetugas.blueGrotto, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField({
|
||||
required String label,
|
||||
required String hint,
|
||||
required TextEditingController controller,
|
||||
bool isRequired = false,
|
||||
int maxLines = 1,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
String? prefixText,
|
||||
IconData? prefixIcon,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
if (isRequired) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'*',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
maxLines: maxLines,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
|
||||
filled: true,
|
||||
fillColor: AppColorsPetugas.babyBlueBright,
|
||||
prefixText: prefixText,
|
||||
prefixIcon:
|
||||
prefixIcon != null
|
||||
? Icon(
|
||||
prefixIcon,
|
||||
size: 20,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategorySelect({
|
||||
required String title,
|
||||
required List<String> options,
|
||||
required RxString selectedOption,
|
||||
required Function(String) onChanged,
|
||||
required IconData icon,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Obx(
|
||||
() => DropdownButtonFormField<String>(
|
||||
value: selectedOption.value,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(
|
||||
icon,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 20,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColorsPetugas.babyBlueBright,
|
||||
),
|
||||
items:
|
||||
options.map((option) {
|
||||
return DropdownMenuItem(
|
||||
value: option,
|
||||
child: Text(
|
||||
option,
|
||||
style: TextStyle(
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) onChanged(value);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
dropdownColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageUploader() {
|
||||
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: [
|
||||
Text(
|
||||
'Unggah Foto Paket',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Tambahkan foto paket untuk informasi visual.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Obx(
|
||||
() => Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
// Add button
|
||||
GestureDetector(
|
||||
onTap: () => controller.addSampleImage(),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: AppColorsPetugas.babyBlue,
|
||||
width: 1,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_photo_alternate_outlined,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tambah Foto',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Image previews
|
||||
...controller.selectedImages.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.removeImage(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: AppColorsPetugas.error,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
label: const Text('Batal'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColorsPetugas.textSecondary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
side: BorderSide(color: AppColorsPetugas.divider),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
final isValid = controller.isFormValid.value;
|
||||
final isSubmitting = controller.isSubmitting.value;
|
||||
return ElevatedButton.icon(
|
||||
onPressed:
|
||||
isValid && !isSubmitting ? controller.savePaket : null,
|
||||
icon:
|
||||
isSubmitting
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.save),
|
||||
label: Text(isSubmitting ? 'Menyimpan...' : 'Simpan Paket'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
disabledBackgroundColor: AppColorsPetugas.textLight,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,149 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
|
||||
class PetugasBumdesBottomNavbar extends StatelessWidget {
|
||||
final int selectedIndex;
|
||||
final Function(int) onItemTapped;
|
||||
|
||||
const PetugasBumdesBottomNavbar({
|
||||
super.key,
|
||||
required this.selectedIndex,
|
||||
required this.onItemTapped,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 76,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.07),
|
||||
blurRadius: 14,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
icon: Icons.dashboard_outlined,
|
||||
activeIcon: Icons.dashboard,
|
||||
label: 'Dashboard',
|
||||
isSelected: selectedIndex == 0,
|
||||
onTap: () => onItemTapped(0),
|
||||
),
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
icon: Icons.inventory_2_outlined,
|
||||
activeIcon: Icons.inventory_2,
|
||||
label: 'Aset',
|
||||
isSelected: selectedIndex == 1,
|
||||
onTap: () => onItemTapped(1),
|
||||
),
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
icon: Icons.category_outlined,
|
||||
activeIcon: Icons.category,
|
||||
label: 'Paket',
|
||||
isSelected: selectedIndex == 2,
|
||||
onTap: () => onItemTapped(2),
|
||||
),
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
icon: Icons.shopping_cart_outlined,
|
||||
activeIcon: Icons.shopping_cart,
|
||||
label: 'Sewa',
|
||||
isSelected: selectedIndex == 3,
|
||||
onTap: () => onItemTapped(3),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Modern navigation item for bottom bar
|
||||
Widget _buildNavItem({
|
||||
required BuildContext context,
|
||||
required IconData icon,
|
||||
required IconData activeIcon,
|
||||
required String label,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final primaryColor = AppColors.primary;
|
||||
final tabWidth = MediaQuery.of(context).size.width / 4;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
customBorder: const StadiumBorder(),
|
||||
splashColor: primaryColor.withOpacity(0.1),
|
||||
highlightColor: primaryColor.withOpacity(0.05),
|
||||
child: SizedBox(
|
||||
width: tabWidth,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Indicator line at top
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
height: 2,
|
||||
width: tabWidth * 0.5,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? primaryColor : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
),
|
||||
),
|
||||
|
||||
// Icon with animated scale effect when selected
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: EdgeInsets.all(isSelected ? 8 : 0),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isSelected
|
||||
? primaryColor.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
isSelected ? activeIcon : icon,
|
||||
color: isSelected ? primaryColor : Colors.grey.shade400,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Label with animated opacity
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: isSelected ? primaryColor : Colors.grey.shade500,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
302
lib/app/modules/petugas_bumdes/widgets/petugas_side_navbar.dart
Normal file
302
lib/app/modules/petugas_bumdes/widgets/petugas_side_navbar.dart
Normal file
@ -0,0 +1,302 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
|
||||
class PetugasSideNavbar extends StatelessWidget {
|
||||
final PetugasBumdesDashboardController controller;
|
||||
|
||||
const PetugasSideNavbar({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(0),
|
||||
bottomRight: Radius.circular(0),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
Expanded(child: _buildMenu()),
|
||||
_buildFooter(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
|
||||
color: AppColors.primary,
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.white,
|
||||
child: Icon(Icons.person, color: AppColors.primary, size: 36),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Petugas BUMDes',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Obx(
|
||||
() => Text(
|
||||
controller.userEmail.value,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenu() {
|
||||
return Obx(
|
||||
() => ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
children: [
|
||||
_buildSectionHeader('Menu Utama'),
|
||||
_buildMenuItem(
|
||||
icon: Icons.dashboard_outlined,
|
||||
activeIcon: Icons.dashboard,
|
||||
title: 'Dashboard',
|
||||
subtitle: 'Ringkasan aktivitas',
|
||||
isSelected: controller.currentTabIndex.value == 0,
|
||||
onTap: () => controller.changeTab(0),
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.inventory_2_outlined,
|
||||
activeIcon: Icons.inventory_2,
|
||||
title: 'Aset',
|
||||
subtitle: 'Kelola aset BUMDes',
|
||||
isSelected: controller.currentTabIndex.value == 1,
|
||||
onTap: () => controller.changeTab(1),
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.category_outlined,
|
||||
activeIcon: Icons.category,
|
||||
title: 'Paket',
|
||||
subtitle: 'Kelola paket aset',
|
||||
isSelected: controller.currentTabIndex.value == 2,
|
||||
onTap: () => controller.changeTab(2),
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.shopping_cart_outlined,
|
||||
activeIcon: Icons.shopping_cart,
|
||||
title: 'Sewa',
|
||||
subtitle: 'Kelola sewa aset',
|
||||
isSelected: controller.currentTabIndex.value == 3,
|
||||
onTap: () => controller.changeTab(3),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade500,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenuItem({
|
||||
required IconData icon,
|
||||
required IconData activeIcon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.primarySoft : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isSelected
|
||||
? AppColors.primary.withOpacity(0.15)
|
||||
: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
isSelected ? activeIcon : icon,
|
||||
color: isSelected ? AppColors.primary : Colors.grey.shade600,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isSelected ? AppColors.primary : Colors.black87,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.w500,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
|
||||
),
|
||||
trailing:
|
||||
isSelected
|
||||
? Container(
|
||||
width: 4,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onTap: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.shade200,
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, -1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(Icons.logout, color: Colors.red.shade400, size: 20),
|
||||
),
|
||||
title: const Text(
|
||||
'Keluar',
|
||||
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 15),
|
||||
),
|
||||
subtitle: const Text(
|
||||
'Keluar dari aplikasi',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
onTap: () => _showLogoutConfirmation(context),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'© 2025 BumRent App',
|
||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Image.asset(
|
||||
'assets/images/logo.png',
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showLogoutConfirmation(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Konfirmasi Keluar'),
|
||||
content: const Text('Apakah Anda yakin ingin keluar dari aplikasi?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.grey.shade700,
|
||||
),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
controller.logout();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade400,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Keluar'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class PetugasMitraDashboardController extends GetxController {
|
||||
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
||||
|
||||
// Observable user data
|
||||
final userEmail = ''.obs;
|
||||
final currentTabIndex = 0.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_loadUserEmail();
|
||||
}
|
||||
|
||||
// Load user email from auth provider
|
||||
Future<void> _loadUserEmail() async {
|
||||
try {
|
||||
final user = _authProvider.currentUser;
|
||||
userEmail.value = user?.email ?? 'User';
|
||||
} catch (e) {
|
||||
debugPrint('Error loading user email: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Change tab index
|
||||
void changeTab(int index) {
|
||||
currentTabIndex.value = index;
|
||||
}
|
||||
|
||||
// Logout function
|
||||
void logout() async {
|
||||
try {
|
||||
await _authProvider.signOut();
|
||||
Get.offAllNamed(Routes.LOGIN);
|
||||
} catch (e) {
|
||||
debugPrint('Error signing out: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal keluar dari aplikasi',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
lib/app/modules/splash/controllers/splash_controller.dart
Normal file
28
lib/app/modules/splash/controllers/splash_controller.dart
Normal file
@ -0,0 +1,28 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class SplashController extends GetxController {
|
||||
late Timer _timer;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
debugPrint('SplashController onInit called');
|
||||
|
||||
// Menggunakan Timer alih-alih Future.delayed
|
||||
_timer = Timer(const Duration(seconds: 3), () {
|
||||
debugPrint('Timer completed, navigating to LOGIN');
|
||||
// Gunakan Get.offAll untuk menghapus semua rute sebelumnya
|
||||
Get.offAllNamed(Routes.LOGIN);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// Pastikan timer dibatalkan saat controller ditutup
|
||||
_timer.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
197
lib/app/modules/splash/views/splash_view.dart
Normal file
197
lib/app/modules/splash/views/splash_view.dart
Normal file
@ -0,0 +1,197 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
import '../controllers/splash_controller.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
|
||||
class SplashView extends GetView<SplashController> {
|
||||
const SplashView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topRight,
|
||||
end: Alignment.bottomLeft,
|
||||
colors: [
|
||||
AppColors.primaryLight.withOpacity(0.1),
|
||||
AppColors.background,
|
||||
AppColors.accentLight.withOpacity(0.1),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Pattern overlay
|
||||
Opacity(
|
||||
opacity: 0.03,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/pattern.png'),
|
||||
repeat: ImageRepeat.repeat,
|
||||
scale: 4.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Accent circles
|
||||
Positioned(
|
||||
top: -40,
|
||||
right: -20,
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 150,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
AppColors.primary.withOpacity(0.2),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
bottom: -50,
|
||||
left: -30,
|
||||
child: Container(
|
||||
width: 180,
|
||||
height: 180,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
AppColors.accent.withOpacity(0.2),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Main content
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Animated Logo
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
duration: const Duration(seconds: 1),
|
||||
curve: Curves.easeOutBack,
|
||||
builder: (context, value, child) {
|
||||
return Transform.scale(
|
||||
scale: value,
|
||||
child: Image.asset(
|
||||
'assets/images/logo.png',
|
||||
width: 180,
|
||||
height: 180,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Icon(
|
||||
Icons.business,
|
||||
size: 100,
|
||||
color: AppColors.primary,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Animated loading indicator
|
||||
_DelayedAnimation(
|
||||
delay: 400,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
AppColors.primary,
|
||||
),
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Animation helper class
|
||||
class _DelayedAnimation extends StatefulWidget {
|
||||
final Widget child;
|
||||
final int delay;
|
||||
|
||||
const _DelayedAnimation({required this.child, required this.delay});
|
||||
|
||||
@override
|
||||
_DelayedAnimationState createState() => _DelayedAnimationState();
|
||||
}
|
||||
|
||||
class _DelayedAnimationState extends State<_DelayedAnimation>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<Offset> _animOffset;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
);
|
||||
|
||||
final curve = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.decelerate,
|
||||
);
|
||||
|
||||
_animOffset = Tween<Offset>(
|
||||
begin: const Offset(0.0, 0.35),
|
||||
end: Offset.zero,
|
||||
).animate(curve);
|
||||
|
||||
Future.delayed(Duration(milliseconds: widget.delay), () {
|
||||
if (mounted) {
|
||||
_controller.forward();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: _controller,
|
||||
child: SlideTransition(position: _animOffset, child: widget.child),
|
||||
);
|
||||
}
|
||||
}
|
||||
75
lib/app/modules/warga/bindings/order_sewa_aset_binding.dart
Normal file
75
lib/app/modules/warga/bindings/order_sewa_aset_binding.dart
Normal file
@ -0,0 +1,75 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../controllers/order_sewa_aset_controller.dart';
|
||||
|
||||
class OrderSewaAsetBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
debugPrint('⚡ OrderSewaAsetBinding: dependencies called');
|
||||
final box = GetStorage();
|
||||
|
||||
// Ensure providers are registered
|
||||
if (!Get.isRegistered<AsetProvider>()) {
|
||||
debugPrint('⚡ Registering AsetProvider');
|
||||
Get.put(AsetProvider(), permanent: true);
|
||||
}
|
||||
|
||||
if (!Get.isRegistered<AuthProvider>()) {
|
||||
debugPrint('⚡ Registering AuthProvider');
|
||||
Get.put(AuthProvider(), permanent: true);
|
||||
}
|
||||
|
||||
// Check if we have the asetId in arguments
|
||||
final args = Get.arguments;
|
||||
debugPrint('⚡ Arguments received in binding: $args');
|
||||
String? asetId;
|
||||
|
||||
if (args != null && args.containsKey('asetId') && args['asetId'] != null) {
|
||||
asetId = args['asetId'].toString();
|
||||
if (asetId.isNotEmpty) {
|
||||
debugPrint('✅ Valid asetId found in arguments: $asetId');
|
||||
// Simpan ID di storage untuk digunakan saat hot reload
|
||||
box.write('current_aset_id', asetId);
|
||||
debugPrint('💾 Saved asetId to GetStorage in binding: $asetId');
|
||||
} else {
|
||||
debugPrint('⚠️ Warning: Empty asetId found in arguments');
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'⚠️ Warning: No valid asetId found in arguments, checking storage',
|
||||
);
|
||||
// Cek apakah ada ID tersimpan di storage
|
||||
if (box.hasData('current_aset_id')) {
|
||||
asetId = box.read<String>('current_aset_id');
|
||||
debugPrint('📦 Found asetId in GetStorage: $asetId');
|
||||
}
|
||||
}
|
||||
|
||||
// Only delete the existing controller if we're not in a hot reload situation
|
||||
if (Get.isRegistered<OrderSewaAsetController>()) {
|
||||
// Check if we're going through a hot reload by looking at the controller's state
|
||||
final existingController = Get.find<OrderSewaAsetController>();
|
||||
if (existingController.aset.value == null) {
|
||||
// Controller exists but doesn't have data, likely a fresh navigation or reload
|
||||
debugPrint('⚡ Removing old OrderSewaAsetController without data');
|
||||
Get.delete<OrderSewaAsetController>(force: true);
|
||||
|
||||
// Use put instead of lazyPut to ensure controller is created immediately
|
||||
debugPrint('⚡ Creating new OrderSewaAsetController');
|
||||
Get.put(OrderSewaAsetController());
|
||||
} else {
|
||||
// Controller exists and has data, leave it alone during hot reload
|
||||
debugPrint(
|
||||
'🔥 Hot reload detected, preserving existing controller with data',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// No controller exists, create a new one
|
||||
debugPrint('⚡ Creating new OrderSewaAsetController (first time)');
|
||||
Get.put(OrderSewaAsetController());
|
||||
}
|
||||
}
|
||||
}
|
||||
22
lib/app/modules/warga/bindings/order_sewa_paket_binding.dart
Normal file
22
lib/app/modules/warga/bindings/order_sewa_paket_binding.dart
Normal file
@ -0,0 +1,22 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/order_sewa_paket_controller.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
import '../../../data/providers/sewa_provider.dart';
|
||||
|
||||
class OrderSewaPaketBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Ensure providers are registered
|
||||
if (!Get.isRegistered<AsetProvider>()) {
|
||||
Get.put(AsetProvider());
|
||||
}
|
||||
|
||||
if (!Get.isRegistered<SewaProvider>()) {
|
||||
Get.put(SewaProvider());
|
||||
}
|
||||
|
||||
Get.lazyPut<OrderSewaPaketController>(
|
||||
() => OrderSewaPaketController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/pembayaran_sewa_controller.dart';
|
||||
|
||||
class PembayaranSewaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<PembayaranSewaController>(() => PembayaranSewaController());
|
||||
}
|
||||
}
|
||||
16
lib/app/modules/warga/bindings/sewa_aset_binding.dart
Normal file
16
lib/app/modules/warga/bindings/sewa_aset_binding.dart
Normal file
@ -0,0 +1,16 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/sewa_aset_controller.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
|
||||
class SewaAsetBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Register AsetProvider if not already registered
|
||||
if (!Get.isRegistered<AsetProvider>()) {
|
||||
Get.put(AsetProvider(), permanent: true);
|
||||
}
|
||||
|
||||
// Register SewaAsetController
|
||||
Get.lazyPut<SewaAsetController>(() => SewaAsetController());
|
||||
}
|
||||
}
|
||||
34
lib/app/modules/warga/bindings/warga_sewa_binding.dart
Normal file
34
lib/app/modules/warga/bindings/warga_sewa_binding.dart
Normal file
@ -0,0 +1,34 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/warga_sewa_controller.dart';
|
||||
import '../controllers/warga_dashboard_controller.dart';
|
||||
import '../../../services/navigation_service.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
|
||||
class WargaSewaBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Ensure NavigationService is registered and set to Sewa tab
|
||||
if (Get.isRegistered<NavigationService>()) {
|
||||
final navService = Get.find<NavigationService>();
|
||||
navService.setNavIndex(1); // Set to Sewa tab
|
||||
}
|
||||
|
||||
// Ensure AuthProvider is registered
|
||||
if (!Get.isRegistered<AuthProvider>()) {
|
||||
Get.put(AuthProvider(), permanent: true);
|
||||
}
|
||||
|
||||
// Ensure AsetProvider is registered
|
||||
if (!Get.isRegistered<AsetProvider>()) {
|
||||
Get.put(AsetProvider(), permanent: true);
|
||||
}
|
||||
|
||||
// Register WargaDashboardController if not already registered
|
||||
if (!Get.isRegistered<WargaDashboardController>()) {
|
||||
Get.put(WargaDashboardController());
|
||||
}
|
||||
|
||||
Get.lazyPut<WargaSewaController>(() => WargaSewaController());
|
||||
}
|
||||
}
|
||||
2364
lib/app/modules/warga/controllers/order_sewa_aset_controller.dart
Normal file
2364
lib/app/modules/warga/controllers/order_sewa_aset_controller.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,443 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter_logs/flutter_logs.dart';
|
||||
import '../../../data/models/paket_model.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
import '../../../data/providers/sewa_provider.dart';
|
||||
import '../../../services/service_manager.dart';
|
||||
import '../../../services/navigation_service.dart';
|
||||
|
||||
class OrderSewaPaketController extends GetxController {
|
||||
// Dependencies
|
||||
final AsetProvider asetProvider = Get.find<AsetProvider>();
|
||||
final SewaProvider sewaProvider = Get.find<SewaProvider>();
|
||||
final NavigationService navigationService = ServiceManager().navigationService;
|
||||
|
||||
// State variables
|
||||
final paket = Rx<PaketModel?>(null);
|
||||
final paketImages = RxList<String>([]);
|
||||
final isLoading = RxBool(true);
|
||||
final isPhotosLoading = RxBool(true);
|
||||
final selectedSatuanWaktu = Rx<Map<String, dynamic>?>(null);
|
||||
final selectedDate = RxString('');
|
||||
final selectedStartDate = Rx<DateTime?>(null);
|
||||
final selectedEndDate = Rx<DateTime?>(null);
|
||||
final selectedStartTime = RxInt(-1);
|
||||
final selectedEndTime = RxInt(-1);
|
||||
final formattedDateRange = RxString('');
|
||||
final formattedTimeRange = RxString('');
|
||||
final totalPrice = RxDouble(0.0);
|
||||
final kuantitas = RxInt(1);
|
||||
final isSubmitting = RxBool(false);
|
||||
|
||||
// Format currency
|
||||
final currencyFormat = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
FlutterLogs.logInfo("OrderSewaPaketController", "onInit", "Initializing OrderSewaPaketController");
|
||||
|
||||
// Get the paket ID from arguments
|
||||
final Map<String, dynamic> args = Get.arguments ?? {};
|
||||
final String? paketId = args['id'];
|
||||
|
||||
if (paketId != null) {
|
||||
loadPaketData(paketId);
|
||||
} else {
|
||||
debugPrint('❌ No paket ID provided in arguments');
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle hot reload - restore state if needed
|
||||
void handleHotReload() {
|
||||
if (paket.value == null) {
|
||||
final Map<String, dynamic> args = Get.arguments ?? {};
|
||||
final String? paketId = args['id'];
|
||||
|
||||
if (paketId != null) {
|
||||
// Try to get from cache first
|
||||
final cachedPaket = GetStorage().read('cached_paket_$paketId');
|
||||
if (cachedPaket != null) {
|
||||
debugPrint('🔄 Hot reload: Restoring paket from cache');
|
||||
paket.value = cachedPaket;
|
||||
loadPaketPhotos(paketId);
|
||||
initializePriceOptions();
|
||||
} else {
|
||||
loadPaketData(paketId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load paket data from API
|
||||
Future<void> loadPaketData(String id) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
debugPrint('🔍 Loading paket data for ID: $id');
|
||||
|
||||
// First check if we have it in cache
|
||||
final cachedPaket = GetStorage().read('cached_paket_$id');
|
||||
if (cachedPaket != null) {
|
||||
debugPrint('✅ Found cached paket data');
|
||||
paket.value = cachedPaket;
|
||||
await loadPaketPhotos(id);
|
||||
initializePriceOptions();
|
||||
} else {
|
||||
// Get all pakets and filter for the one we need
|
||||
final List<dynamic> allPakets = await asetProvider.getPakets();
|
||||
final rawPaket = allPakets.firstWhere(
|
||||
(paket) => paket['id'] == id,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
// Declare loadedPaket outside the if block for wider scope
|
||||
PaketModel? loadedPaket;
|
||||
|
||||
if (rawPaket != null) {
|
||||
// Convert to PaketModel
|
||||
try {
|
||||
// Handle Map directly - pakets from getPakets() are always maps
|
||||
loadedPaket = PaketModel.fromMap(rawPaket);
|
||||
debugPrint('✅ Successfully converted paket to PaketModel');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error converting paket map to PaketModel: $e');
|
||||
// Fallback using our helper methods
|
||||
loadedPaket = PaketModel(
|
||||
id: getPaketId(rawPaket),
|
||||
nama: getPaketNama(rawPaket),
|
||||
deskripsi: getPaketDeskripsi(rawPaket),
|
||||
harga: getPaketHarga(rawPaket),
|
||||
kuantitas: getPaketKuantitas(rawPaket),
|
||||
foto_paket: getPaketMainPhoto(rawPaket),
|
||||
satuanWaktuSewa: getPaketSatuanWaktuSewa(rawPaket),
|
||||
);
|
||||
debugPrint('✅ Created PaketModel using helper methods');
|
||||
}
|
||||
|
||||
// Update the state with the loaded paket
|
||||
if (loadedPaket != null) {
|
||||
debugPrint('✅ Loaded paket: ${loadedPaket.nama}');
|
||||
paket.value = loadedPaket;
|
||||
|
||||
// Cache for future use
|
||||
GetStorage().write('cached_paket_$id', loadedPaket);
|
||||
|
||||
// Load photos for this paket
|
||||
await loadPaketPhotos(id);
|
||||
|
||||
// Set initial pricing option
|
||||
initializePriceOptions();
|
||||
|
||||
// Ensure we have at least one photo if available
|
||||
if (paketImages.isEmpty) {
|
||||
String? mainPhoto = getPaketMainPhoto(paket.value);
|
||||
if (mainPhoto != null && mainPhoto.isNotEmpty) {
|
||||
paketImages.add(mainPhoto);
|
||||
debugPrint('✅ Added main paket photo: $mainPhoto');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugPrint('❌ No paket found with id: $id');
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the total price if we have a paket loaded
|
||||
if (paket.value != null) {
|
||||
calculateTotalPrice();
|
||||
debugPrint('💰 Total price calculated: ${totalPrice.value}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error loading paket data: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods to safely access paket properties
|
||||
String? getPaketId(dynamic paket) {
|
||||
if (paket == null) return null;
|
||||
try {
|
||||
return paket.id ?? paket['id'];
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String? getPaketNama(dynamic paket) {
|
||||
if (paket == null) return null;
|
||||
try {
|
||||
return paket.nama ?? paket['nama'];
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String? getPaketDeskripsi(dynamic paket) {
|
||||
if (paket == null) return null;
|
||||
try {
|
||||
return paket.deskripsi ?? paket['deskripsi'];
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
double getPaketHarga(dynamic paket) {
|
||||
if (paket == null) return 0.0;
|
||||
try {
|
||||
var harga = paket.harga ?? paket['harga'] ?? 0;
|
||||
return double.tryParse(harga.toString()) ?? 0.0;
|
||||
} catch (_) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
int getPaketKuantitas(dynamic paket) {
|
||||
if (paket == null) return 1;
|
||||
try {
|
||||
var qty = paket.kuantitas ?? paket['kuantitas'] ?? 1;
|
||||
return int.tryParse(qty.toString()) ?? 1;
|
||||
} catch (_) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
String? getPaketMainPhoto(dynamic paket) {
|
||||
if (paket == null) return null;
|
||||
try {
|
||||
return paket.foto_paket ?? paket['foto_paket'];
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<dynamic> getPaketSatuanWaktuSewa(dynamic paket) {
|
||||
if (paket == null) return [];
|
||||
try {
|
||||
return paket.satuanWaktuSewa ?? paket['satuanWaktuSewa'] ?? [];
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Load photos for the paket
|
||||
Future<void> loadPaketPhotos(String paketId) async {
|
||||
try {
|
||||
isPhotosLoading.value = true;
|
||||
final photos = await asetProvider.getFotoPaket(paketId);
|
||||
if (photos != null && photos.isNotEmpty) {
|
||||
paketImages.clear();
|
||||
for (var photo in photos) {
|
||||
try {
|
||||
if (photo.fotoPaket != null && photo.fotoPaket.isNotEmpty) {
|
||||
paketImages.add(photo.fotoPaket);
|
||||
} else if (photo.fotoAset != null && photo.fotoAset.isNotEmpty) {
|
||||
paketImages.add(photo.fotoAset);
|
||||
}
|
||||
} catch (e) {
|
||||
var fotoUrl = photo['foto_paket'] ?? photo['foto_aset'];
|
||||
if (fotoUrl != null && fotoUrl.isNotEmpty) {
|
||||
paketImages.add(fotoUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isPhotosLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize price options
|
||||
void initializePriceOptions() {
|
||||
if (paket.value == null) return;
|
||||
|
||||
final satuanWaktuSewa = getPaketSatuanWaktuSewa(paket.value);
|
||||
if (satuanWaktuSewa.isNotEmpty) {
|
||||
// Default to the first option
|
||||
selectSatuanWaktu(satuanWaktuSewa.first);
|
||||
}
|
||||
}
|
||||
|
||||
// Select satuan waktu
|
||||
void selectSatuanWaktu(Map<String, dynamic> satuanWaktu) {
|
||||
selectedSatuanWaktu.value = satuanWaktu;
|
||||
|
||||
// Reset date and time selections
|
||||
selectedStartDate.value = null;
|
||||
selectedEndDate.value = null;
|
||||
selectedStartTime.value = -1;
|
||||
selectedEndTime.value = -1;
|
||||
selectedDate.value = '';
|
||||
formattedDateRange.value = '';
|
||||
formattedTimeRange.value = '';
|
||||
|
||||
calculateTotalPrice();
|
||||
}
|
||||
|
||||
// Check if the rental is daily
|
||||
bool isDailyRental() {
|
||||
final namaSatuan = selectedSatuanWaktu.value?['nama_satuan_waktu'] ?? '';
|
||||
return namaSatuan.toString().toLowerCase().contains('hari');
|
||||
}
|
||||
|
||||
// Select date range for daily rental
|
||||
void selectDateRange(DateTime start, DateTime end) {
|
||||
selectedStartDate.value = start;
|
||||
selectedEndDate.value = end;
|
||||
|
||||
// Format the date range
|
||||
final formatter = DateFormat('d MMM yyyy', 'id');
|
||||
if (start.year == end.year && start.month == end.month && start.day == end.day) {
|
||||
formattedDateRange.value = formatter.format(start);
|
||||
} else {
|
||||
formattedDateRange.value = '${formatter.format(start)} - ${formatter.format(end)}';
|
||||
}
|
||||
|
||||
selectedDate.value = formatter.format(start);
|
||||
calculateTotalPrice();
|
||||
}
|
||||
|
||||
// Select date for hourly rental
|
||||
void selectDate(DateTime date) {
|
||||
selectedStartDate.value = date;
|
||||
selectedDate.value = DateFormat('d MMM yyyy', 'id').format(date);
|
||||
calculateTotalPrice();
|
||||
}
|
||||
|
||||
// Select time range for hourly rental
|
||||
void selectTimeRange(int start, int end) {
|
||||
selectedStartTime.value = start;
|
||||
selectedEndTime.value = end;
|
||||
|
||||
// Format the time range
|
||||
final startTime = '$start:00';
|
||||
final endTime = '$end:00';
|
||||
formattedTimeRange.value = '$startTime - $endTime';
|
||||
|
||||
calculateTotalPrice();
|
||||
}
|
||||
|
||||
// Calculate total price
|
||||
void calculateTotalPrice() {
|
||||
if (selectedSatuanWaktu.value == null) {
|
||||
totalPrice.value = 0.0;
|
||||
return;
|
||||
}
|
||||
|
||||
final basePrice = double.tryParse(selectedSatuanWaktu.value!['harga'].toString()) ?? 0.0;
|
||||
|
||||
if (isDailyRental()) {
|
||||
if (selectedStartDate.value != null && selectedEndDate.value != null) {
|
||||
final days = selectedEndDate.value!.difference(selectedStartDate.value!).inDays + 1;
|
||||
totalPrice.value = basePrice * days;
|
||||
} else {
|
||||
totalPrice.value = basePrice;
|
||||
}
|
||||
} else {
|
||||
if (selectedStartTime.value >= 0 && selectedEndTime.value >= 0) {
|
||||
final hours = selectedEndTime.value - selectedStartTime.value;
|
||||
totalPrice.value = basePrice * hours;
|
||||
} else {
|
||||
totalPrice.value = basePrice;
|
||||
}
|
||||
}
|
||||
|
||||
// Multiply by quantity
|
||||
totalPrice.value *= kuantitas.value;
|
||||
}
|
||||
|
||||
// Format price as currency
|
||||
String formatPrice(double price) {
|
||||
return currencyFormat.format(price);
|
||||
}
|
||||
|
||||
// Submit order
|
||||
Future<void> submitOrder() async {
|
||||
try {
|
||||
if (paket.value == null || selectedSatuanWaktu.value == null) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Data paket tidak lengkap',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((isDailyRental() && (selectedStartDate.value == null || selectedEndDate.value == null)) ||
|
||||
(!isDailyRental() && (selectedStartDate.value == null || selectedStartTime.value < 0 || selectedEndTime.value < 0))) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Silakan pilih waktu sewa',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
|
||||
// Prepare order data
|
||||
final Map<String, dynamic> orderData = {
|
||||
'id_paket': paket.value!.id,
|
||||
'id_satuan_waktu_sewa': selectedSatuanWaktu.value!['id'],
|
||||
'tanggal_mulai': selectedStartDate.value!.toIso8601String(),
|
||||
'tanggal_selesai': selectedEndDate.value?.toIso8601String() ?? selectedStartDate.value!.toIso8601String(),
|
||||
'jam_mulai': isDailyRental() ? null : selectedStartTime.value,
|
||||
'jam_selesai': isDailyRental() ? null : selectedEndTime.value,
|
||||
'total_harga': totalPrice.value,
|
||||
'kuantitas': kuantitas.value,
|
||||
};
|
||||
|
||||
// Submit the order
|
||||
final result = await sewaProvider.createPaketOrder(orderData);
|
||||
|
||||
if (result != null) {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Pesanan berhasil dibuat',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
// Navigate to payment page
|
||||
navigationService.navigateToPembayaranSewa(result['id']);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal membuat pesanan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error submitting order: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan: $e',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle back button press
|
||||
void onBackPressed() {
|
||||
navigationService.navigateToSewaAset();
|
||||
}
|
||||
}
|
||||
1115
lib/app/modules/warga/controllers/pembayaran_sewa_controller.bak
Normal file
1115
lib/app/modules/warga/controllers/pembayaran_sewa_controller.bak
Normal file
File diff suppressed because it is too large
Load Diff
1202
lib/app/modules/warga/controllers/pembayaran_sewa_controller.dart
Normal file
1202
lib/app/modules/warga/controllers/pembayaran_sewa_controller.dart
Normal file
File diff suppressed because it is too large
Load Diff
471
lib/app/modules/warga/controllers/sewa_aset_controller.dart
Normal file
471
lib/app/modules/warga/controllers/sewa_aset_controller.dart
Normal file
@ -0,0 +1,471 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
import '../../../data/models/aset_model.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import '../../../data/models/pesanan_model.dart';
|
||||
import '../../../data/models/satuan_waktu_model.dart';
|
||||
import '../../../data/models/satuan_waktu_sewa_model.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../data/providers/pesanan_provider.dart';
|
||||
import '../../../services/navigation_service.dart';
|
||||
import '../../../services/service_manager.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
class SewaAsetController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
||||
final AuthProvider authProvider = Get.find<AuthProvider>();
|
||||
final PesananProvider pesananProvider = Get.put(PesananProvider());
|
||||
final NavigationService navigationService = Get.find<NavigationService>();
|
||||
final box = GetStorage();
|
||||
|
||||
// Tab controller
|
||||
late TabController tabController;
|
||||
// Reactive tab index
|
||||
final currentTabIndex = 0.obs;
|
||||
|
||||
// State variables
|
||||
final asets = <AsetModel>[].obs;
|
||||
final filteredAsets = <AsetModel>[].obs;
|
||||
|
||||
// Paket-related variables
|
||||
final pakets = RxList<dynamic>([]);
|
||||
final filteredPakets = RxList<dynamic>([]);
|
||||
final isLoadingPakets = false.obs;
|
||||
|
||||
final isLoading = true.obs;
|
||||
|
||||
// Search controller
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
// Reactive variables
|
||||
final isOrdering = false.obs;
|
||||
final selectedAset = Rx<AsetModel?>(null);
|
||||
final selectedSatuanWaktuSewa = Rx<SatuanWaktuSewaModel?>(null);
|
||||
final selectedDurasi = 1.obs;
|
||||
final totalHarga = 0.obs;
|
||||
final selectedDate = DateTime.now().obs;
|
||||
final selectedTime = '08:00'.obs;
|
||||
final satuanWaktuDropdownItems =
|
||||
<DropdownMenuItem<SatuanWaktuSewaModel>>[].obs;
|
||||
|
||||
// Flag untuk menangani hot reload
|
||||
final hasInitialized = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
debugPrint('🚀 SewaAsetController: onInit called');
|
||||
|
||||
// Initialize tab controller
|
||||
tabController = TabController(length: 2, vsync: this);
|
||||
// Listen for tab changes
|
||||
tabController.addListener(() {
|
||||
currentTabIndex.value = tabController.index;
|
||||
|
||||
// Load packages data when switching to package tab for the first time
|
||||
if (currentTabIndex.value == 1 && pakets.isEmpty) {
|
||||
loadPakets();
|
||||
}
|
||||
});
|
||||
|
||||
loadAsets();
|
||||
|
||||
searchController.addListener(() {
|
||||
if (currentTabIndex.value == 0) {
|
||||
filterAsets(searchController.text);
|
||||
} else {
|
||||
filterPakets(searchController.text);
|
||||
}
|
||||
});
|
||||
|
||||
hasInitialized.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
debugPrint('🚀 SewaAsetController: onReady called');
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
debugPrint('🧹 SewaAsetController: onClose called');
|
||||
searchController.dispose();
|
||||
tabController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Method untuk menangani hot reload
|
||||
void handleHotReload() {
|
||||
debugPrint('🔥 Hot reload detected in SewaAsetController');
|
||||
if (!hasInitialized.value) {
|
||||
debugPrint('🔄 Reinitializing SewaAsetController after hot reload');
|
||||
loadAsets();
|
||||
if (currentTabIndex.value == 1) {
|
||||
loadPakets();
|
||||
}
|
||||
hasInitialized.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Method untuk menangani tombol back
|
||||
void onBackPressed() {
|
||||
debugPrint('🔙 Back button pressed in SewaAsetView');
|
||||
navigationService.backFromSewaAset();
|
||||
}
|
||||
|
||||
Future<void> loadAsets() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
final sewaAsets = await _asetProvider.getSewaAsets();
|
||||
|
||||
// Debug data satuan waktu sewa yang diterima
|
||||
debugPrint('===== DEBUG ASET & SATUAN WAKTU SEWA =====');
|
||||
for (var aset in sewaAsets) {
|
||||
debugPrint('Aset: ${aset.nama} (ID: ${aset.id})');
|
||||
|
||||
if (aset.satuanWaktuSewa.isEmpty) {
|
||||
debugPrint(' - Tidak ada satuan waktu sewa yang terkait');
|
||||
} else {
|
||||
debugPrint(
|
||||
' - Memiliki ${aset.satuanWaktuSewa.length} satuan waktu sewa:',
|
||||
);
|
||||
for (var sws in aset.satuanWaktuSewa) {
|
||||
debugPrint(' * ID: ${sws['id']}');
|
||||
debugPrint(' Aset ID: ${sws['aset_id']}');
|
||||
debugPrint(' Satuan Waktu ID: ${sws['satuan_waktu_id']}');
|
||||
debugPrint(' Harga: ${sws['harga']}');
|
||||
debugPrint(' Nama Satuan Waktu: ${sws['nama_satuan_waktu']}');
|
||||
debugPrint(' -----');
|
||||
}
|
||||
}
|
||||
debugPrint('=====================================');
|
||||
}
|
||||
|
||||
asets.assignAll(sewaAsets);
|
||||
filteredAsets.assignAll(sewaAsets);
|
||||
|
||||
// Tambahkan log info tentang jumlah aset yang berhasil dimuat
|
||||
debugPrint('Loaded ${sewaAsets.length} aset sewa successfully');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading asets: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan saat memuat data aset',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void filterAsets(String query) {
|
||||
if (query.isEmpty) {
|
||||
filteredAsets.assignAll(asets);
|
||||
} else {
|
||||
filteredAsets.assignAll(
|
||||
asets
|
||||
.where(
|
||||
(aset) => aset.nama.toLowerCase().contains(query.toLowerCase()),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void refreshAsets() {
|
||||
loadAsets();
|
||||
}
|
||||
|
||||
String formatPrice(dynamic price) {
|
||||
if (price == null) return 'Rp 0';
|
||||
|
||||
// Handle different types
|
||||
num numericPrice;
|
||||
if (price is int || price is double) {
|
||||
numericPrice = price;
|
||||
} else if (price is String) {
|
||||
numericPrice = double.tryParse(price) ?? 0;
|
||||
} else {
|
||||
return 'Rp 0';
|
||||
}
|
||||
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
return formatter.format(numericPrice);
|
||||
}
|
||||
|
||||
void selectAset(AsetModel aset) {
|
||||
selectedAset.value = aset;
|
||||
// Reset related values
|
||||
selectedSatuanWaktuSewa.value = null;
|
||||
selectedDurasi.value = 1;
|
||||
totalHarga.value = 0;
|
||||
|
||||
// Prepare dropdown items for satuan waktu sewa
|
||||
updateSatuanWaktuDropdown();
|
||||
}
|
||||
|
||||
void updateSatuanWaktuDropdown() {
|
||||
satuanWaktuDropdownItems.clear();
|
||||
|
||||
if (selectedAset.value != null &&
|
||||
selectedAset.value!.satuanWaktuSewa.isNotEmpty) {
|
||||
for (var item in selectedAset.value!.satuanWaktuSewa) {
|
||||
final satuanWaktuSewa = SatuanWaktuSewaModel.fromJson(item);
|
||||
satuanWaktuDropdownItems.add(
|
||||
DropdownMenuItem<SatuanWaktuSewaModel>(
|
||||
value: satuanWaktuSewa,
|
||||
child: Text(
|
||||
'${satuanWaktuSewa.namaSatuanWaktu ?? "Unknown"} - Rp${NumberFormat.decimalPattern('id').format(satuanWaktuSewa.harga)}',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void selectSatuanWaktu(SatuanWaktuSewaModel? satuanWaktuSewa) {
|
||||
selectedSatuanWaktuSewa.value = satuanWaktuSewa;
|
||||
calculateTotalPrice();
|
||||
}
|
||||
|
||||
void updateDurasi(int durasi) {
|
||||
if (durasi < 1) durasi = 1;
|
||||
selectedDurasi.value = durasi;
|
||||
calculateTotalPrice();
|
||||
}
|
||||
|
||||
void calculateTotalPrice() {
|
||||
if (selectedSatuanWaktuSewa.value != null) {
|
||||
totalHarga.value =
|
||||
selectedSatuanWaktuSewa.value!.harga * selectedDurasi.value;
|
||||
} else {
|
||||
totalHarga.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void pickDate(DateTime date) {
|
||||
selectedDate.value = date;
|
||||
}
|
||||
|
||||
void pickTime(String time) {
|
||||
selectedTime.value = time;
|
||||
}
|
||||
|
||||
// Helper method to show error snackbar
|
||||
void _showError(String message) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
message,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
|
||||
// Method untuk melakukan pemesanan
|
||||
Future<void> placeOrderAset() async {
|
||||
if (selectedAset.value == null) {
|
||||
_showError('Silakan pilih aset terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedSatuanWaktuSewa.value == null) {
|
||||
_showError('Silakan pilih satuan waktu sewa');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedDurasi.value <= 0) {
|
||||
_showError('Durasi sewa harus lebih dari 0');
|
||||
return;
|
||||
}
|
||||
|
||||
final userId = authProvider.getCurrentUserId();
|
||||
if (userId == null) {
|
||||
_showError('Anda belum login, silakan login terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final result = await _asetProvider.orderAset(
|
||||
userId: userId,
|
||||
asetId: selectedAset.value!.id,
|
||||
satuanWaktuSewaId: selectedSatuanWaktuSewa.value!.id,
|
||||
durasi: selectedDurasi.value,
|
||||
totalHarga: totalHarga.value,
|
||||
);
|
||||
|
||||
if (result) {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Pesanan berhasil dibuat',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
resetSelections();
|
||||
} else {
|
||||
_showError('Gagal membuat pesanan');
|
||||
}
|
||||
} catch (e) {
|
||||
_showError('Terjadi kesalahan: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Method untuk reset pilihan setelah pemesanan berhasil
|
||||
void resetSelections() {
|
||||
selectedAset.value = null;
|
||||
selectedSatuanWaktuSewa.value = null;
|
||||
selectedDurasi.value = 1;
|
||||
totalHarga.value = 0;
|
||||
}
|
||||
|
||||
// Load packages data from paket table
|
||||
Future<void> loadPakets() async {
|
||||
try {
|
||||
isLoadingPakets.value = true;
|
||||
|
||||
// Call the provider method to get paket data
|
||||
final paketData = await _asetProvider.getPakets();
|
||||
|
||||
// Debug paket data
|
||||
debugPrint('===== DEBUG PAKET & SATUAN WAKTU SEWA =====');
|
||||
for (var paket in paketData) {
|
||||
debugPrint('Paket: ${paket['nama']} (ID: ${paket['id']})');
|
||||
|
||||
if (paket['satuanWaktuSewa'] == null ||
|
||||
paket['satuanWaktuSewa'].isEmpty) {
|
||||
debugPrint(' - Tidak ada satuan waktu sewa yang terkait');
|
||||
} else {
|
||||
debugPrint(
|
||||
' - Memiliki ${paket['satuanWaktuSewa'].length} satuan waktu sewa:',
|
||||
);
|
||||
for (var sws in paket['satuanWaktuSewa']) {
|
||||
debugPrint(' * ID: ${sws['id']}');
|
||||
debugPrint(' Paket ID: ${sws['paket_id']}');
|
||||
debugPrint(' Satuan Waktu ID: ${sws['satuan_waktu_id']}');
|
||||
debugPrint(' Harga: ${sws['harga']}');
|
||||
debugPrint(' Nama Satuan Waktu: ${sws['nama_satuan_waktu']}');
|
||||
debugPrint(' -----');
|
||||
}
|
||||
}
|
||||
debugPrint('=====================================');
|
||||
}
|
||||
|
||||
pakets.assignAll(paketData);
|
||||
filteredPakets.assignAll(paketData);
|
||||
|
||||
debugPrint('Loaded ${paketData.length} paket successfully');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading pakets: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan saat memuat data paket',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoadingPakets.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to filter pakets based on search query
|
||||
void filterPakets(String query) {
|
||||
if (query.isEmpty) {
|
||||
filteredPakets.assignAll(pakets);
|
||||
} else {
|
||||
filteredPakets.assignAll(
|
||||
pakets
|
||||
.where(
|
||||
(paket) => paket['nama'].toString().toLowerCase().contains(
|
||||
query.toLowerCase(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void refreshPakets() {
|
||||
loadPakets();
|
||||
}
|
||||
|
||||
// Method to load paket data
|
||||
Future<void> loadPaketData() async {
|
||||
try {
|
||||
isLoadingPakets.value = true;
|
||||
final result = await _asetProvider.getPakets();
|
||||
if (result != null) {
|
||||
pakets.clear();
|
||||
filteredPakets.clear();
|
||||
pakets.addAll(result);
|
||||
filteredPakets.addAll(result);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading pakets: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memuat data paket. Silakan coba lagi nanti.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoadingPakets.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Method for placing an order for a paket
|
||||
Future<void> placeOrderPaket({
|
||||
required String paketId,
|
||||
required String satuanWaktuSewaId,
|
||||
required int durasi,
|
||||
required int totalHarga,
|
||||
}) async {
|
||||
debugPrint('===== PLACE ORDER PAKET =====');
|
||||
debugPrint('paketId: $paketId');
|
||||
debugPrint('satuanWaktuSewaId: $satuanWaktuSewaId');
|
||||
debugPrint('durasi: $durasi');
|
||||
debugPrint('totalHarga: $totalHarga');
|
||||
|
||||
final userId = authProvider.getCurrentUserId();
|
||||
if (userId == null) {
|
||||
_showError('Anda belum login, silakan login terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final result = await _asetProvider.orderPaket(
|
||||
userId: userId,
|
||||
paketId: paketId,
|
||||
satuanWaktuSewaId: satuanWaktuSewaId,
|
||||
durasi: durasi,
|
||||
totalHarga: totalHarga,
|
||||
);
|
||||
|
||||
if (result) {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Pesanan paket berhasil dibuat',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} else {
|
||||
_showError('Gagal membuat pesanan paket');
|
||||
}
|
||||
} catch (e) {
|
||||
_showError('Terjadi kesalahan: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,180 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import '../../../services/navigation_service.dart';
|
||||
|
||||
class WargaDashboardController extends GetxController {
|
||||
// Dependency injection
|
||||
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
||||
final NavigationService navigationService = Get.find<NavigationService>();
|
||||
|
||||
// User data
|
||||
final userName = 'Pengguna Warga'.obs;
|
||||
final userRole = 'Warga'.obs;
|
||||
final userAvatar = Rx<String?>(null);
|
||||
final userEmail = ''.obs;
|
||||
final userNik = ''.obs;
|
||||
final userPhone = ''.obs;
|
||||
final userAddress = ''.obs;
|
||||
|
||||
// Navigation state is now managed by NavigationService
|
||||
|
||||
// Sample data (would be loaded from API)
|
||||
final activeRentals = <Map<String, dynamic>>[].obs;
|
||||
|
||||
// Active bills
|
||||
final activeBills = <Map<String, dynamic>>[].obs;
|
||||
|
||||
// Active penalties
|
||||
final activePenalties = <Map<String, dynamic>>[].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// Set navigation index to Home (0)
|
||||
navigationService.setNavIndex(0);
|
||||
|
||||
// Load user data
|
||||
_loadUserData();
|
||||
|
||||
// Load sample data
|
||||
_loadSampleData();
|
||||
|
||||
// Load dummy data for bills and penalties
|
||||
loadDummyData();
|
||||
|
||||
// Load unpaid rentals
|
||||
loadUnpaidRentals();
|
||||
}
|
||||
|
||||
Future<void> _loadUserData() async {
|
||||
try {
|
||||
// Get the full name from warga_desa table
|
||||
final fullName = await _authProvider.getUserFullName();
|
||||
if (fullName != null && fullName.isNotEmpty) {
|
||||
userName.value = fullName;
|
||||
}
|
||||
|
||||
// Get the avatar URL
|
||||
final avatar = await _authProvider.getUserAvatar();
|
||||
userAvatar.value = avatar;
|
||||
|
||||
// Get the role name
|
||||
final roleId = await _authProvider.getUserRoleId();
|
||||
if (roleId != null) {
|
||||
final roleName = await _authProvider.getRoleName(roleId);
|
||||
if (roleName != null) {
|
||||
userRole.value = roleName;
|
||||
}
|
||||
}
|
||||
|
||||
// Load additional user data
|
||||
// In a real app, these would come from the API/database
|
||||
userEmail.value = await _authProvider.getUserEmail() ?? '';
|
||||
userNik.value = await _authProvider.getUserNIK() ?? '';
|
||||
userPhone.value = await _authProvider.getUserPhone() ?? '';
|
||||
userAddress.value = await _authProvider.getUserAddress() ?? '';
|
||||
} catch (e) {
|
||||
print('Error loading user data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _loadSampleData() {
|
||||
// Clear any existing data
|
||||
activeRentals.clear();
|
||||
|
||||
// Load active rentals from API
|
||||
// For now, using sample data
|
||||
activeRentals.add({
|
||||
'id': '1',
|
||||
'name': 'Kursi',
|
||||
'time': '24 April 2023, 10:00 - 12:00',
|
||||
'duration': '2 jam',
|
||||
'price': 'Rp50.000',
|
||||
'can_extend': true,
|
||||
});
|
||||
}
|
||||
|
||||
void extendRental(String rentalId) {
|
||||
// Implementasi untuk memperpanjang sewa
|
||||
// Seharusnya melakukan API call ke backend
|
||||
}
|
||||
|
||||
void endRental(String rentalId) {
|
||||
// Implementasi untuk mengakhiri sewa
|
||||
// Seharusnya melakukan API call ke backend
|
||||
}
|
||||
|
||||
void navigateToRentals() {
|
||||
// Navigate to SewaAset using the navigation service
|
||||
navigationService.toSewaAset();
|
||||
}
|
||||
|
||||
void refreshData() {
|
||||
// Refresh data from repository
|
||||
_loadSampleData();
|
||||
loadDummyData();
|
||||
}
|
||||
|
||||
void onNavItemTapped(int index) {
|
||||
if (navigationService.currentNavIndex.value == index) {
|
||||
return; // Don't do anything if same tab
|
||||
}
|
||||
|
||||
navigationService.setNavIndex(index);
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
// Already on Home tab
|
||||
break;
|
||||
case 1:
|
||||
// Navigate to Sewa page
|
||||
navigationService.toWargaSewa();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void logout() async {
|
||||
await _authProvider.signOut();
|
||||
navigationService.toLogin();
|
||||
}
|
||||
|
||||
void loadDummyData() {
|
||||
// Dummy active bills
|
||||
activeBills.clear();
|
||||
activeBills.add({
|
||||
'id': '1',
|
||||
'title': 'Tagihan Air',
|
||||
'due_date': '30 Apr 2023',
|
||||
'amount': 'Rp 125.000',
|
||||
});
|
||||
activeBills.add({
|
||||
'id': '2',
|
||||
'title': 'Sewa Aula Desa',
|
||||
'due_date': '15 Apr 2023',
|
||||
'amount': 'Rp 350.000',
|
||||
});
|
||||
|
||||
// Dummy active penalties
|
||||
activePenalties.clear();
|
||||
activePenalties.add({
|
||||
'id': '1',
|
||||
'title': 'Keterlambatan Sewa Traktor',
|
||||
'days_late': '7',
|
||||
'amount': 'Rp 75.000',
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> loadUnpaidRentals() async {
|
||||
try {
|
||||
final results = await _authProvider.getSewaAsetByStatus([
|
||||
'MENUNGGU PEMBAYARAN',
|
||||
'PEMBAYARANAN DENDA',
|
||||
]);
|
||||
activeBills.value = results;
|
||||
} catch (e) {
|
||||
print('Error loading unpaid rentals: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
710
lib/app/modules/warga/controllers/warga_sewa_controller.dart
Normal file
710
lib/app/modules/warga/controllers/warga_sewa_controller.dart
Normal file
@ -0,0 +1,710 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import '../../../services/navigation_service.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
|
||||
class WargaSewaController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
late TabController tabController;
|
||||
|
||||
// Get navigation service
|
||||
final NavigationService navigationService = Get.find<NavigationService>();
|
||||
|
||||
// Get auth provider for user data and sewa_aset queries
|
||||
final AuthProvider authProvider = Get.find<AuthProvider>();
|
||||
|
||||
// Get aset provider for asset data
|
||||
final AsetProvider asetProvider = Get.find<AsetProvider>();
|
||||
|
||||
// Observable lists for different rental statuses
|
||||
final rentals = <Map<String, dynamic>>[].obs;
|
||||
final pendingRentals = <Map<String, dynamic>>[].obs;
|
||||
final acceptedRentals = <Map<String, dynamic>>[].obs;
|
||||
final completedRentals = <Map<String, dynamic>>[].obs;
|
||||
final cancelledRentals = <Map<String, dynamic>>[].obs;
|
||||
|
||||
// Loading states
|
||||
final isLoading = false.obs;
|
||||
final isLoadingPending = false.obs;
|
||||
final isLoadingAccepted = false.obs;
|
||||
final isLoadingCompleted = false.obs;
|
||||
final isLoadingCancelled = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// Ensure tab index is set to Sewa (1)
|
||||
navigationService.setNavIndex(1);
|
||||
|
||||
// Initialize tab controller with 6 tabs
|
||||
tabController = TabController(length: 6, vsync: this);
|
||||
|
||||
// Set initial tab and ensure tab view is updated
|
||||
tabController.index = 0;
|
||||
|
||||
// Load real rental data for all tabs
|
||||
loadRentalsData();
|
||||
loadPendingRentals();
|
||||
loadAcceptedRentals();
|
||||
loadCompletedRentals();
|
||||
loadCancelledRentals();
|
||||
|
||||
// Listen to tab changes to update state if needed
|
||||
tabController.addListener(() {
|
||||
// Update selected tab index when changed via swipe
|
||||
final int currentIndex = tabController.index;
|
||||
debugPrint('Tab changed to index: $currentIndex');
|
||||
|
||||
// Load data for the selected tab if not already loaded
|
||||
switch (currentIndex) {
|
||||
case 0: // Belum Bayar
|
||||
if (rentals.isEmpty && !isLoading.value) {
|
||||
loadRentalsData();
|
||||
}
|
||||
break;
|
||||
case 1: // Pending
|
||||
if (pendingRentals.isEmpty && !isLoadingPending.value) {
|
||||
loadPendingRentals();
|
||||
}
|
||||
break;
|
||||
case 2: // Diterima
|
||||
if (acceptedRentals.isEmpty && !isLoadingAccepted.value) {
|
||||
loadAcceptedRentals();
|
||||
}
|
||||
break;
|
||||
case 3: // Aktif
|
||||
// Add Aktif tab logic when needed
|
||||
break;
|
||||
case 4: // Selesai
|
||||
if (completedRentals.isEmpty && !isLoadingCompleted.value) {
|
||||
loadCompletedRentals();
|
||||
}
|
||||
break;
|
||||
case 5: // Dibatalkan
|
||||
if (cancelledRentals.isEmpty && !isLoadingCancelled.value) {
|
||||
loadCancelledRentals();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
// Ensure nav index is set to Sewa (1) when the controller is ready
|
||||
// This helps maintain correct state during hot reload
|
||||
navigationService.setNavIndex(1);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
tabController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Load real data from sewa_aset table
|
||||
Future<void> loadRentalsData() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// Clear existing data
|
||||
rentals.clear();
|
||||
|
||||
// Get sewa_aset data with status "MENUNGGU PEMBAYARAN" or "PEMBAYARAN DENDA"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||
'MENUNGGU PEMBAYARAN',
|
||||
'PEMBAYARAN DENDA'
|
||||
]);
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} sewa_aset records');
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to rentals list
|
||||
rentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'MENUNGGU PEMBAYARAN',
|
||||
'totalPrice': totalPrice,
|
||||
'countdown': '00:59:59', // Default countdown
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
});
|
||||
}
|
||||
|
||||
debugPrint('Processed ${rentals.length} rental records');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading rentals data: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation methods
|
||||
void navigateToRentals() {
|
||||
navigationService.toSewaAset();
|
||||
}
|
||||
|
||||
void onNavItemTapped(int index) {
|
||||
if (navigationService.currentNavIndex.value == index) return;
|
||||
|
||||
navigationService.setNavIndex(index);
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
// Navigate to Home
|
||||
Get.offNamed(Routes.WARGA_DASHBOARD);
|
||||
break;
|
||||
case 1:
|
||||
// Already on Sewa tab
|
||||
break;
|
||||
case 2:
|
||||
// Navigate to Langganan
|
||||
Get.offNamed(Routes.LANGGANAN);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
void cancelRental(String id) {
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Pembatalan berhasil',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
// Navigate to payment page with the selected rental data
|
||||
void viewRentalDetail(Map<String, dynamic> rental) {
|
||||
debugPrint('Navigating to payment page with rental ID: ${rental['id']}');
|
||||
|
||||
// Navigate to payment page with rental data
|
||||
Get.toNamed(
|
||||
Routes.PEMBAYARAN_SEWA,
|
||||
arguments: {
|
||||
'orderId': rental['id'],
|
||||
'rentalData': rental,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void payRental(String id) {
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Navigasi ke halaman pembayaran',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
// Load data for the Selesai tab (status: SELESAI)
|
||||
Future<void> loadCompletedRentals() async {
|
||||
try {
|
||||
isLoadingCompleted.value = true;
|
||||
|
||||
// Clear existing data
|
||||
completedRentals.clear();
|
||||
|
||||
// Get sewa_aset data with status "SELESAI"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['SELESAI']);
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} completed sewa_aset records');
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to completed rentals list
|
||||
completedRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'SELESAI',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
});
|
||||
}
|
||||
|
||||
debugPrint('Processed ${completedRentals.length} completed rental records');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading completed rentals data: $e');
|
||||
} finally {
|
||||
isLoadingCompleted.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load data for the Dibatalkan tab (status: DIBATALKAN)
|
||||
Future<void> loadCancelledRentals() async {
|
||||
try {
|
||||
isLoadingCancelled.value = true;
|
||||
|
||||
// Clear existing data
|
||||
cancelledRentals.clear();
|
||||
|
||||
// Get sewa_aset data with status "DIBATALKAN"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['DIBATALKAN']);
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} cancelled sewa_aset records');
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to cancelled rentals list
|
||||
cancelledRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'DIBATALKAN',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
'alasanPembatalan': sewaAset['alasan_pembatalan'] ?? '-',
|
||||
});
|
||||
}
|
||||
|
||||
debugPrint('Processed ${cancelledRentals.length} cancelled rental records');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading cancelled rentals data: $e');
|
||||
} finally {
|
||||
isLoadingCancelled.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load data for the Pending tab (status: PERIKSA PEMBAYARAN)
|
||||
Future<void> loadPendingRentals() async {
|
||||
try {
|
||||
isLoadingPending.value = true;
|
||||
|
||||
// Clear existing data
|
||||
pendingRentals.clear();
|
||||
|
||||
// Get sewa_aset data with status "PERIKSA PEMBAYARAN"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['PERIKSA PEMBAYARAN']);
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} pending sewa_aset records');
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to pending rentals list
|
||||
pendingRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'PERIKSA PEMBAYARAN',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
});
|
||||
}
|
||||
|
||||
debugPrint('Processed ${pendingRentals.length} pending rental records');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading pending rentals data: $e');
|
||||
} finally {
|
||||
isLoadingPending.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load data for the Diterima tab (status: DITERIMA)
|
||||
Future<void> loadAcceptedRentals() async {
|
||||
try {
|
||||
isLoadingAccepted.value = true;
|
||||
|
||||
// Clear existing data
|
||||
acceptedRentals.clear();
|
||||
|
||||
// Get sewa_aset data with status "DITERIMA"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['DITERIMA']);
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} accepted sewa_aset records');
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to accepted rentals list
|
||||
acceptedRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'DITERIMA',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
});
|
||||
}
|
||||
|
||||
debugPrint('Processed ${acceptedRentals.length} accepted rental records');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading accepted rentals data: $e');
|
||||
} finally {
|
||||
isLoadingAccepted.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
2178
lib/app/modules/warga/views/order_sewa_aset_view.dart
Normal file
2178
lib/app/modules/warga/views/order_sewa_aset_view.dart
Normal file
File diff suppressed because it is too large
Load Diff
981
lib/app/modules/warga/views/order_sewa_paket_view.dart
Normal file
981
lib/app/modules/warga/views/order_sewa_paket_view.dart
Normal file
@ -0,0 +1,981 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import '../controllers/order_sewa_paket_controller.dart';
|
||||
import '../../../data/models/paket_model.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import '../../../services/navigation_service.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:photo_view/photo_view_gallery.dart';
|
||||
import 'package:flutter_logs/flutter_logs.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class OrderSewaPaketView extends GetView<OrderSewaPaketController> {
|
||||
const OrderSewaPaketView({super.key});
|
||||
|
||||
// Function to show confirmation dialog
|
||||
void showOrderConfirmationDialog() {
|
||||
final paket = controller.paket.value!;
|
||||
final PaketModel? paketModel = paket is PaketModel ? paket : null;
|
||||
final totalPrice = controller.totalPrice.value;
|
||||
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header with success icon
|
||||
Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primarySoft,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check_circle_outline_rounded,
|
||||
color: AppColors.primary,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
|
||||
// Title
|
||||
Text(
|
||||
'Konfirmasi Pesanan',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
|
||||
// Subtitle
|
||||
Text(
|
||||
'Periksa detail pesanan Anda',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
|
||||
// Order details
|
||||
Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceLight,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Paket name
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Paket',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
paketModel?.nama ?? controller.getPaketNama(paket) ?? 'Paket tanpa nama',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(height: 24, color: AppColors.divider),
|
||||
|
||||
// Duration info
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Durasi',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Text(
|
||||
controller.isDailyRental()
|
||||
? controller.formattedDateRange.value
|
||||
: '${controller.selectedDate.value}, ${controller.formattedTimeRange.value}',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(height: 24, color: AppColors.divider),
|
||||
|
||||
// Total price info
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Total',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Text(
|
||||
controller.formatPrice(controller.totalPrice.value),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
side: BorderSide(color: AppColors.primary),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Batal',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => ElevatedButton(
|
||||
onPressed: controller.isSubmitting.value
|
||||
? null
|
||||
: () {
|
||||
Get.back();
|
||||
controller.submitOrder();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: AppColors.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: controller.isSubmitting.value
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'Pesan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Handle hot reload by checking if controller needs to be reset
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// This will be called after the widget tree is built
|
||||
controller.handleHotReload();
|
||||
|
||||
// Ensure navigation service is registered for back button functionality
|
||||
if (!Get.isRegistered<NavigationService>()) {
|
||||
Get.put(NavigationService());
|
||||
debugPrint('✅ Created new NavigationService instance in view');
|
||||
}
|
||||
});
|
||||
|
||||
// Function to handle back button press
|
||||
void handleBackButtonPress() {
|
||||
debugPrint('🔙 Back button pressed - navigating to SewaAsetView');
|
||||
try {
|
||||
// First try to use the controller's method
|
||||
controller.onBackPressed();
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Error handling back via controller: $e');
|
||||
// Fallback to direct navigation
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: AppColors.textPrimary),
|
||||
onPressed: handleBackButtonPress,
|
||||
),
|
||||
title: Text(
|
||||
'Pesan Paket',
|
||||
style: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Obx(
|
||||
() => controller.isLoading.value
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: controller.paket.value == null
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline_rounded,
|
||||
size: 64,
|
||||
color: AppColors.error,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Paket tidak ditemukan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Silakan kembali dan pilih paket lain',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: handleBackButtonPress,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text('Kembali'),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTopSection(),
|
||||
_buildPaketDetails(),
|
||||
_buildPriceOptions(),
|
||||
_buildDateSelection(context),
|
||||
SizedBox(height: 100), // Space for bottom bar
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomSheet: Obx(
|
||||
() => controller.isLoading.value || controller.paket.value == null
|
||||
? SizedBox.shrink()
|
||||
: _buildBottomBar(onTapPesan: showOrderConfirmationDialog),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Build top section with paket images
|
||||
Widget _buildTopSection() {
|
||||
return Container(
|
||||
height: 280,
|
||||
width: double.infinity,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Photo gallery
|
||||
Obx(
|
||||
() => controller.isPhotosLoading.value
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: controller.paketImages.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.image_not_supported_outlined,
|
||||
size: 64,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Tidak ada foto',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: PhotoViewGallery.builder(
|
||||
scrollPhysics: BouncingScrollPhysics(),
|
||||
builder: (BuildContext context, int index) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider: CachedNetworkImageProvider(
|
||||
controller.paketImages[index],
|
||||
),
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
maxScale: PhotoViewComputedScale.covered * 2,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: 'paket_image_$index',
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: controller.paketImages.length,
|
||||
loadingBuilder: (context, event) => Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: Colors.black,
|
||||
),
|
||||
pageController: PageController(),
|
||||
),
|
||||
),
|
||||
|
||||
// Gradient overlay at the top for back button
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 80,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.5),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Build paket details section
|
||||
Widget _buildPaketDetails() {
|
||||
final paket = controller.paket.value!;
|
||||
final PaketModel? paketModel = paket is PaketModel ? paket : null;
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Paket name and availability badge
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
paketModel?.nama ?? controller.getPaketNama(paket) ?? 'Paket tanpa nama',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.success.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Tersedia',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.success,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Description
|
||||
Text(
|
||||
'Deskripsi',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
paketModel?.deskripsi ?? controller.getPaketDeskripsi(paket) ?? 'Tidak ada deskripsi untuk paket ini.',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
| ||||