Perbarui model PenitipanBantuan untuk menggunakan stok_bantuan sebagai kategori bantuan. Tambahkan rute baru untuk Donatur dengan beberapa halaman terkait, termasuk donaturDashboard, donaturSkema, donaturJadwal, donaturPenitipan, dan donaturLaporan. Modifikasi rute dan tampilan untuk meningkatkan navigasi dan pengalaman pengguna.
This commit is contained in:
774
lib/app/modules/donatur/views/donatur_dashboard_view.dart
Normal file
774
lib/app/modules/donatur/views/donatur_dashboard_view.dart
Normal file
@ -0,0 +1,774 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/routes/app_pages.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
|
||||
class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
||||
const DonaturDashboardView({super.key});
|
||||
|
||||
@override
|
||||
DonaturDashboardController get controller {
|
||||
if (!Get.isRegistered<DonaturDashboardController>(
|
||||
tag: 'donatur_dashboard')) {
|
||||
return Get.put(DonaturDashboardController(),
|
||||
tag: 'donatur_dashboard', permanent: true);
|
||||
}
|
||||
return Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
controller.fetchData();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildWelcomeSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildStatisticSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildUpcomingEvents(),
|
||||
const SizedBox(height: 24),
|
||||
_buildRecentPenitipan(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWelcomeSection() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.white, Colors.blue.shade50],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.blue.shade200, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.blue.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Hero(
|
||||
tag: 'donatur-profile',
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.blue.shade100,
|
||||
backgroundImage: controller.profilePhotoUrl != null
|
||||
? NetworkImage(controller.profilePhotoUrl!)
|
||||
: null,
|
||||
child: controller.profilePhotoUrl == null
|
||||
? Icon(
|
||||
Icons.person,
|
||||
color: Colors.blue.shade700,
|
||||
size: 30,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Selamat Datang,',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.blue.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
controller.nama,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade900,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.blue.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildInfoRow(
|
||||
icon: Icons.home_rounded,
|
||||
iconColor: Colors.blue.shade300,
|
||||
label: 'Alamat',
|
||||
value: controller.alamat ?? 'Alamat tidak tersedia',
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Divider(height: 1),
|
||||
),
|
||||
_buildInfoRow(
|
||||
icon: Icons.phone_rounded,
|
||||
iconColor: Colors.green.shade300,
|
||||
label: 'No. HP',
|
||||
value: controller.noHp ?? 'No. HP tidak tersedia',
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Divider(height: 1),
|
||||
),
|
||||
_buildInfoRow(
|
||||
icon: Icons.category_rounded,
|
||||
iconColor: Colors.amber.shade300,
|
||||
label: 'Jenis Donatur',
|
||||
value: controller.jenis ?? 'Jenis tidak tersedia',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildActionButton(
|
||||
icon: Icons.edit_rounded,
|
||||
label: 'Edit Profil',
|
||||
color: Colors.blue.shade700,
|
||||
onTap: () => Get.toNamed(Routes.profile),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _buildActionButton(
|
||||
icon: Icons.add_box_rounded,
|
||||
label: 'Titip Bantuan',
|
||||
color: Colors.green.shade700,
|
||||
onTap: () {
|
||||
// Navigasi ke form penitipan bantuan
|
||||
controller.activeTabIndex.value = 3;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatisticSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Statistik Kontribusi',
|
||||
),
|
||||
Text(
|
||||
'Ringkasan aktivitas anda sebagai donatur',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
title: 'Total Bantuan',
|
||||
value: '${controller.penitipanBantuan.length}',
|
||||
icon: Icons.card_giftcard,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
title: 'Menunggu Verifikasi',
|
||||
value:
|
||||
'${controller.penitipanBantuan.where((p) => p.status == 'MENUNGGU').length}',
|
||||
icon: Icons.hourglass_empty,
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
title: 'Diterima',
|
||||
value:
|
||||
'${controller.penitipanBantuan.where((p) => p.status == 'DITERIMA').length}',
|
||||
icon: Icons.check_circle_outline,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
title: 'Ditolak',
|
||||
value:
|
||||
'${controller.penitipanBantuan.where((p) => p.status == 'DITOLAK').length}',
|
||||
icon: Icons.cancel_outlined,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUpcomingEvents() {
|
||||
final upcomingEvents = controller.jadwalPenyaluran
|
||||
.where((event) =>
|
||||
event.tanggalPenyaluran != null &&
|
||||
event.tanggalPenyaluran!.isAfter(DateTime.now()))
|
||||
.take(3)
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Jadwal Penyaluran',
|
||||
),
|
||||
Text(
|
||||
'Jadwal penyaluran bantuan terdekat',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Navigasi ke tab jadwal penyaluran
|
||||
controller.activeTabIndex.value = 2;
|
||||
},
|
||||
child: Text(
|
||||
'Lihat Semua',
|
||||
style: TextStyle(color: Colors.blue.shade700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (upcomingEvents.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Tidak ada jadwal penyaluran dalam waktu dekat',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
...upcomingEvents.map((event) => _buildEventCard(event)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecentPenitipan() {
|
||||
final recentPenitipan = controller.penitipanBantuan.take(3).toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Bantuan Terakhir',
|
||||
),
|
||||
Text(
|
||||
'Riwayat penitipan bantuan terakhir',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Navigasi ke tab riwayat penitipan
|
||||
controller.activeTabIndex.value = 3;
|
||||
},
|
||||
child: Text(
|
||||
'Lihat Semua',
|
||||
style: TextStyle(color: Colors.blue.shade700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (recentPenitipan.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Belum ada riwayat penitipan bantuan',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
...recentPenitipan.map((penitipan) => _buildPenitipanCard(penitipan)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow({
|
||||
required IconData icon,
|
||||
required Color iconColor,
|
||||
required String label,
|
||||
required String value,
|
||||
}) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: iconColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return ElevatedButton(
|
||||
onPressed: onTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: color,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(label),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatCard({
|
||||
required String title,
|
||||
required String value,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventCard(dynamic event) {
|
||||
final formattedDate = event.tanggalPenyaluran != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID').format(event.tanggalPenyaluran!)
|
||||
: 'Tanggal tidak tersedia';
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.event,
|
||||
color: Colors.blue.shade700,
|
||||
size: 24,
|
||||
),
|
||||
Text(
|
||||
event.tanggalPenyaluran != null
|
||||
? DateFormat('dd').format(event.tanggalPenyaluran!)
|
||||
: '--',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
event.nama ?? 'Penyaluran Bantuan',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
formattedDate,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
event.deskripsi ?? 'Tidak ada deskripsi',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPenitipanCard(dynamic penitipan) {
|
||||
final formattedDate = penitipan.tanggalPenitipan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(penitipan.tanggalPenitipan!)
|
||||
: 'Tanggal tidak tersedia';
|
||||
|
||||
Color statusColor;
|
||||
IconData statusIcon;
|
||||
|
||||
switch (penitipan.status) {
|
||||
case 'DITERIMA':
|
||||
statusColor = Colors.green;
|
||||
statusIcon = Icons.check_circle;
|
||||
break;
|
||||
case 'DITOLAK':
|
||||
statusColor = Colors.red;
|
||||
statusIcon = Icons.cancel;
|
||||
break;
|
||||
case 'MENUNGGU':
|
||||
default:
|
||||
statusColor = Colors.orange;
|
||||
statusIcon = Icons.hourglass_empty;
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
statusIcon,
|
||||
color: statusColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
penitipan.kategoriBantuan?.nama ?? 'Bantuan',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
penitipan.status ?? 'MENUNGGU',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
formattedDate,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Jumlah: ${penitipan.jumlah ?? 0}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
322
lib/app/modules/donatur/views/donatur_jadwal_view.dart
Normal file
322
lib/app/modules/donatur/views/donatur_jadwal_view.dart
Normal file
@ -0,0 +1,322 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
|
||||
class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
||||
const DonaturJadwalView({super.key});
|
||||
|
||||
@override
|
||||
DonaturDashboardController get controller {
|
||||
if (!Get.isRegistered<DonaturDashboardController>(
|
||||
tag: 'donatur_dashboard')) {
|
||||
return Get.put(DonaturDashboardController(),
|
||||
tag: 'donatur_dashboard', permanent: true);
|
||||
}
|
||||
return Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.fetchJadwalPenyaluran();
|
||||
},
|
||||
child: controller.jadwalPenyaluran.isEmpty
|
||||
? _buildEmptyState()
|
||||
: _buildJadwalList(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.event_busy,
|
||||
size: 80,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Belum Ada Jadwal Penyaluran',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
'Jadwal penyaluran bantuan belum tersedia saat ini',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => controller.fetchJadwalPenyaluran(),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Muat Ulang'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.blue,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildJadwalList() {
|
||||
// Kelompokkan jadwal berdasarkan bulan
|
||||
Map<String, List<dynamic>> groupedJadwal = {};
|
||||
|
||||
for (var jadwal in controller.jadwalPenyaluran) {
|
||||
if (jadwal.tanggalPenyaluran != null) {
|
||||
String monthYear =
|
||||
DateFormat('MMMM yyyy', 'id_ID').format(jadwal.tanggalPenyaluran!);
|
||||
|
||||
if (!groupedJadwal.containsKey(monthYear)) {
|
||||
groupedJadwal[monthYear] = [];
|
||||
}
|
||||
|
||||
groupedJadwal[monthYear]!.add(jadwal);
|
||||
}
|
||||
}
|
||||
|
||||
// Urutkan kunci (bulan) secara kronologis
|
||||
List<String> sortedMonths = groupedJadwal.keys.toList()
|
||||
..sort((a, b) {
|
||||
DateTime dateA = DateFormat('MMMM yyyy', 'id_ID').parse(a);
|
||||
DateTime dateB = DateFormat('MMMM yyyy', 'id_ID').parse(b);
|
||||
return dateA.compareTo(dateB);
|
||||
});
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const SectionHeader(title: 'Jadwal Penyaluran Bantuan'),
|
||||
Text(
|
||||
'Daftar jadwal penyaluran bantuan yang akan datang',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Tampilkan jadwal berdasarkan bulan
|
||||
...sortedMonths
|
||||
.map((month) => _buildMonthSection(month, groupedJadwal[month]!)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthSection(String month, List<dynamic> jadwalList) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
month,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
),
|
||||
...jadwalList.map((jadwal) => _buildJadwalCard(jadwal)),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildJadwalCard(dynamic jadwal) {
|
||||
final formattedDate = jadwal.tanggalPenyaluran != null
|
||||
? DateFormat('EEEE, dd MMMM yyyy', 'id_ID')
|
||||
.format(jadwal.tanggalPenyaluran!)
|
||||
: 'Tanggal tidak tersedia';
|
||||
|
||||
String statusText = 'Akan Datang';
|
||||
Color statusColor = Colors.blue;
|
||||
|
||||
switch (jadwal.status) {
|
||||
case 'SELESAI':
|
||||
statusText = 'Selesai';
|
||||
statusColor = Colors.green;
|
||||
break;
|
||||
case 'DIBATALKAN':
|
||||
statusText = 'Dibatalkan';
|
||||
statusColor = Colors.red;
|
||||
break;
|
||||
case 'DALAM_PROSES':
|
||||
statusText = 'Dalam Proses';
|
||||
statusColor = Colors.orange;
|
||||
break;
|
||||
default:
|
||||
statusText = 'Akan Datang';
|
||||
statusColor = Colors.blue;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (jadwal.tanggalPenyaluran != null) ...[
|
||||
Text(
|
||||
DateFormat('dd').format(jadwal.tanggalPenyaluran!),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('MMM', 'id_ID')
|
||||
.format(jadwal.tanggalPenyaluran!)
|
||||
.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
] else
|
||||
Icon(
|
||||
Icons.event,
|
||||
color: statusColor,
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
jadwal.nama ?? 'Penyaluran Bantuan',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
formattedDate,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (jadwal.deskripsi != null && jadwal.deskripsi!.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
jadwal.deskripsi!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
409
lib/app/modules/donatur/views/donatur_laporan_view.dart
Normal file
409
lib/app/modules/donatur/views/donatur_laporan_view.dart
Normal file
@ -0,0 +1,409 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
|
||||
class DonaturLaporanView extends GetView<DonaturDashboardController> {
|
||||
const DonaturLaporanView({super.key});
|
||||
|
||||
@override
|
||||
DonaturDashboardController get controller {
|
||||
if (!Get.isRegistered<DonaturDashboardController>(
|
||||
tag: 'donatur_dashboard')) {
|
||||
return Get.put(DonaturDashboardController(),
|
||||
tag: 'donatur_dashboard', permanent: true);
|
||||
}
|
||||
return Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.fetchLaporanPenyaluran();
|
||||
},
|
||||
child: controller.laporanPenyaluran.isEmpty
|
||||
? _buildEmptyState()
|
||||
: _buildLaporanList(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.assignment_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Belum Ada Laporan Penyaluran',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
'Laporan penyaluran bantuan belum tersedia',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => controller.fetchLaporanPenyaluran(),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Muat Ulang'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.blue,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLaporanList() {
|
||||
// Urutkan laporan berdasarkan tanggal, yang terbaru di atas
|
||||
final sortedLaporan = controller.laporanPenyaluran.toList()
|
||||
..sort((a, b) {
|
||||
if (a.tanggalLaporan == null || b.tanggalLaporan == null) {
|
||||
return 0;
|
||||
}
|
||||
return b.tanggalLaporan!.compareTo(a.tanggalLaporan!);
|
||||
});
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const SectionHeader(title: 'Laporan Penyaluran Bantuan'),
|
||||
Text(
|
||||
'Daftar laporan hasil penyaluran bantuan',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...sortedLaporan.map((laporan) => _buildLaporanCard(laporan)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLaporanCard(dynamic laporan) {
|
||||
final formattedDate = laporan.tanggalLaporan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID').format(laporan.tanggalLaporan!)
|
||||
: 'Tanggal tidak tersedia';
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: laporan.dokumentasiUrl != null &&
|
||||
laporan.dokumentasiUrl!.isNotEmpty
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Image.network(
|
||||
laporan.dokumentasiUrl!,
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Icon(
|
||||
Icons.assignment,
|
||||
color: Colors.blue.shade700,
|
||||
size: 30,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.assignment,
|
||||
color: Colors.blue.shade700,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
laporan.judul,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
formattedDate,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
if (laporan.beritaAcaraUrl != null &&
|
||||
laporan.beritaAcaraUrl!.isNotEmpty)
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Implementasi untuk membuka berita acara
|
||||
_openDocument(laporan.beritaAcaraUrl!);
|
||||
},
|
||||
icon: const Icon(Icons.description, size: 16),
|
||||
label: const Text('Berita Acara'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.blue,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
minimumSize: const Size(30, 30),
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
if (laporan.dokumentasiUrl != null &&
|
||||
laporan.dokumentasiUrl!.isNotEmpty) ...[
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Implementasi untuk melihat dokumentasi
|
||||
_viewDocumentation(laporan.dokumentasiUrl!);
|
||||
},
|
||||
icon: const Icon(Icons.photo_library, size: 16),
|
||||
label: const Text('Dokumentasi'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.green,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
minimumSize: const Size(30, 30),
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openDocument(String url) {
|
||||
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(
|
||||
'Buka Dokumen',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Anda akan membuka dokumen berita acara. Lanjutkan?',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
// Implementasi untuk membuka URL dokumen
|
||||
// Misalnya menggunakan package url_launcher
|
||||
// launch(url);
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Membuka dokumen: $url',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
child: const Text('Buka'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _viewDocumentation(String url) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
insetPadding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Dokumentasi',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: Get.height * 0.6,
|
||||
maxWidth: Get.width,
|
||||
),
|
||||
child: Image.network(
|
||||
url,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 50,
|
||||
color: Colors.red.shade300,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Gagal memuat gambar'),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
1150
lib/app/modules/donatur/views/donatur_penitipan_view.dart
Normal file
1150
lib/app/modules/donatur/views/donatur_penitipan_view.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,393 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
|
||||
class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
||||
DonaturRiwayatPenitipanView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
DonaturDashboardController get controller {
|
||||
return Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
}
|
||||
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Riwayat Penitipan'),
|
||||
bottom: const TabBar(
|
||||
tabs: [
|
||||
Tab(text: 'Menunggu'),
|
||||
Tab(text: 'Diterima'),
|
||||
Tab(text: 'Ditolak'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
// Tab Menunggu
|
||||
_buildPenitipanList(context, 'MENUNGGU'),
|
||||
// Tab Diterima
|
||||
_buildPenitipanList(context, 'DITERIMA'),
|
||||
// Tab Ditolak
|
||||
_buildPenitipanList(context, 'DITOLAK'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPenitipanList(BuildContext context, String status) {
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
// Filter penitipan berdasarkan status
|
||||
var filteredList = controller.penitipanBantuan
|
||||
.where((item) => item.status == status)
|
||||
.toList();
|
||||
|
||||
// Filter berdasarkan pencarian
|
||||
final searchText = searchController.text.toLowerCase();
|
||||
if (searchText.isNotEmpty) {
|
||||
filteredList = filteredList.where((item) {
|
||||
final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? '';
|
||||
final deskripsi = item.deskripsi?.toLowerCase() ?? '';
|
||||
final tanggal = item.tanggalPenitipan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(item.tanggalPenitipan!)
|
||||
.toLowerCase()
|
||||
: '';
|
||||
|
||||
return kategoriNama.contains(searchText) ||
|
||||
deskripsi.contains(searchText) ||
|
||||
tanggal.contains(searchText);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.fetchPenitipanBantuan();
|
||||
},
|
||||
child: filteredList.isEmpty
|
||||
? _buildEmptyState(status)
|
||||
: _buildContentList(context, filteredList, status),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(String status) {
|
||||
String statusText = '';
|
||||
switch (status) {
|
||||
case 'MENUNGGU':
|
||||
statusText = 'menunggu verifikasi';
|
||||
break;
|
||||
case 'DITERIMA':
|
||||
statusText = 'diterima';
|
||||
break;
|
||||
case 'DITOLAK':
|
||||
statusText = 'ditolak';
|
||||
break;
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Tidak ada penitipan $statusText',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
'Anda belum memiliki riwayat penitipan yang $statusText',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContentList(
|
||||
BuildContext context, List<dynamic> filteredList, String status) {
|
||||
Color statusColor;
|
||||
switch (status) {
|
||||
case 'DITERIMA':
|
||||
statusColor = Colors.green;
|
||||
break;
|
||||
case 'DITOLAK':
|
||||
statusColor = Colors.red;
|
||||
break;
|
||||
case 'MENUNGGU':
|
||||
default:
|
||||
statusColor = Colors.orange;
|
||||
break;
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Search field
|
||||
TextField(
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari riwayat penitipan...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
||||
),
|
||||
onChanged: (value) {
|
||||
// Trigger update dengan GetX
|
||||
controller.update();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Info jumlah item
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Daftar Penitipan ${status.toLowerCase()}',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${filteredList.length} item',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Daftar penitipan
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: filteredList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildPenitipanCard(
|
||||
context, filteredList[index], statusColor);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPenitipanCard(
|
||||
BuildContext context, dynamic penitipan, Color statusColor) {
|
||||
final formattedDate = penitipan.tanggalPenitipan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(penitipan.tanggalPenitipan!)
|
||||
: 'Tanggal tidak tersedia';
|
||||
|
||||
IconData statusIcon;
|
||||
|
||||
switch (penitipan.status) {
|
||||
case 'DITERIMA':
|
||||
statusIcon = Icons.check_circle;
|
||||
break;
|
||||
case 'DITOLAK':
|
||||
statusIcon = Icons.cancel;
|
||||
break;
|
||||
case 'MENUNGGU':
|
||||
default:
|
||||
statusIcon = Icons.hourglass_empty;
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
statusIcon,
|
||||
color: statusColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
penitipan.kategoriBantuan?.nama ?? 'Bantuan',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
penitipan.status ?? 'MENUNGGU',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
formattedDate,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Jumlah: ${penitipan.jumlah ?? 0}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (penitipan.deskripsi != null &&
|
||||
penitipan.deskripsi!.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
penitipan.deskripsi!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
if (penitipan.status == 'DITOLAK' &&
|
||||
penitipan.alasanPenolakan != null &&
|
||||
penitipan.alasanPenolakan!.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
'Alasan Penolakan:',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
penitipan.alasanPenolakan!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.red.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
274
lib/app/modules/donatur/views/donatur_skema_view.dart
Normal file
274
lib/app/modules/donatur/views/donatur_skema_view.dart
Normal file
@ -0,0 +1,274 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
|
||||
class DonaturSkemaView extends GetView<DonaturDashboardController> {
|
||||
const DonaturSkemaView({super.key});
|
||||
|
||||
@override
|
||||
DonaturDashboardController get controller {
|
||||
if (!Get.isRegistered<DonaturDashboardController>(
|
||||
tag: 'donatur_dashboard')) {
|
||||
return Get.put(DonaturDashboardController(),
|
||||
tag: 'donatur_dashboard', permanent: true);
|
||||
}
|
||||
return Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.fetchSkemaBantuan();
|
||||
},
|
||||
child: controller.skemaBantuan.isEmpty
|
||||
? _buildEmptyState()
|
||||
: _buildSkemaList(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.description_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Belum Ada Skema Bantuan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Skema bantuan belum tersedia saat ini',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => controller.fetchSkemaBantuan(),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Muat Ulang'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.blue,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSkemaList() {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const SectionHeader(title: 'Skema Bantuan Tersedia'),
|
||||
Text(
|
||||
'Daftar skema bantuan yang dapat Anda titipkan',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...controller.skemaBantuan.map((skema) => _buildSkemaCard(skema)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSkemaCard(dynamic skema) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.blue.withOpacity(0.08),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.volunteer_activism,
|
||||
color: Colors.blue.shade700,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
skema.nama ?? 'Skema Bantuan',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (skema.kuota != null)
|
||||
_buildInfoChip(
|
||||
icon: Icons.people_outline,
|
||||
label: 'Kuota: ${skema.kuota} penerima',
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
if (skema.jumlahDiterimaPerOrang != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: _buildInfoChip(
|
||||
icon: Icons.inventory_2_outlined,
|
||||
label:
|
||||
'Jumlah per orang: ${skema.jumlahDiterimaPerOrang}',
|
||||
color: Colors.green.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (skema.deskripsi != null && skema.deskripsi!.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Deskripsi',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
skema.deskripsi!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const Divider(height: 24),
|
||||
],
|
||||
),
|
||||
if (skema.kriteria != null && skema.kriteria!.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Kriteria Penerima',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
skema.kriteria!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Navigasi ke formulir penitipan bantuan
|
||||
controller.activeTabIndex.value = 3;
|
||||
},
|
||||
icon: const Icon(Icons.add_box_outlined, size: 18),
|
||||
label: const Text('Titipkan Bantuan'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.green.shade600,
|
||||
minimumSize: const Size(double.infinity, 45),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoChip({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color color,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 14,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
326
lib/app/modules/donatur/views/donatur_view.dart
Normal file
326
lib/app/modules/donatur/views/donatur_view.dart
Normal file
@ -0,0 +1,326 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/views/donatur_dashboard_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/views/donatur_skema_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/views/donatur_jadwal_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/views/donatur_laporan_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/views/donatur_penitipan_view.dart';
|
||||
import 'package:penyaluran_app/app/widgets/app_bottom_navigation_bar.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
|
||||
class DonaturView extends GetView<DonaturDashboardController> {
|
||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
DonaturView({super.key});
|
||||
|
||||
// Override untuk mendapatkan controller dengan tag
|
||||
@override
|
||||
DonaturDashboardController get controller {
|
||||
if (!Get.isRegistered<DonaturDashboardController>(
|
||||
tag: 'donatur_dashboard')) {
|
||||
return Get.put(DonaturDashboardController(),
|
||||
tag: 'donatur_dashboard', permanent: true);
|
||||
}
|
||||
return Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: Obx(() {
|
||||
switch (controller.activeTabIndex.value) {
|
||||
case 0:
|
||||
return const Text('Dashboard Donatur');
|
||||
case 1:
|
||||
return const Text('Skema Bantuan');
|
||||
case 2:
|
||||
return const Text('Jadwal Penyaluran');
|
||||
case 3:
|
||||
return const Text('Penitipan Bantuan');
|
||||
case 4:
|
||||
return const Text('Laporan Penyaluran');
|
||||
default:
|
||||
return const Text('Dashboard Donatur');
|
||||
}
|
||||
}),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
onPressed: () {
|
||||
scaffoldKey.currentState?.openDrawer();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
// Tombol riwayat penitipan khusus untuk tab penitipan bantuan
|
||||
Obx(() => controller.activeTabIndex.value == 3
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.history),
|
||||
onPressed: () {
|
||||
// Navigasi ke halaman riwayat penitipan
|
||||
Get.to(
|
||||
() => DonaturRiwayatPenitipanView(),
|
||||
transition: Transition.rightToLeft,
|
||||
);
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink()),
|
||||
// Tombol notifikasi
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.notifications_outlined),
|
||||
onPressed: () {
|
||||
// Navigasi ke halaman notifikasi
|
||||
Get.toNamed('/notifikasi');
|
||||
},
|
||||
),
|
||||
Obx(() {
|
||||
if (controller.jumlahNotifikasiBelumDibaca.value > 0) {
|
||||
return Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 16,
|
||||
minHeight: 16,
|
||||
),
|
||||
child: Text(
|
||||
controller.jumlahNotifikasiBelumDibaca.value.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
drawer: _buildDrawer(context),
|
||||
body: Obx(() {
|
||||
// Tampilkan sesuai dengan tab yang aktif
|
||||
switch (controller.activeTabIndex.value) {
|
||||
case 0:
|
||||
return const DonaturDashboardView();
|
||||
case 1:
|
||||
return const DonaturSkemaView();
|
||||
case 2:
|
||||
return const DonaturJadwalView();
|
||||
case 3:
|
||||
return const DonaturPenitipanView();
|
||||
case 4:
|
||||
return const DonaturLaporanView();
|
||||
default:
|
||||
return const DonaturDashboardView();
|
||||
}
|
||||
}),
|
||||
bottomNavigationBar: Obx(() => AppBottomNavigationBar(
|
||||
currentIndex: controller.activeTabIndex.value,
|
||||
onTap: (index) {
|
||||
controller.activeTabIndex.value = index;
|
||||
},
|
||||
items: [
|
||||
AppBottomNavigationBarItem(
|
||||
icon: Icons.dashboard_outlined,
|
||||
activeIcon: Icons.dashboard,
|
||||
label: 'Dashboard',
|
||||
),
|
||||
AppBottomNavigationBarItem(
|
||||
icon: Icons.description_outlined,
|
||||
activeIcon: Icons.description,
|
||||
label: 'Skema',
|
||||
),
|
||||
AppBottomNavigationBarItem(
|
||||
icon: Icons.calendar_today_outlined,
|
||||
activeIcon: Icons.calendar_today,
|
||||
label: 'Jadwal',
|
||||
),
|
||||
AppBottomNavigationBarItem(
|
||||
icon: Icons.add_box_outlined,
|
||||
activeIcon: Icons.add_box,
|
||||
label: 'Penitipan',
|
||||
),
|
||||
AppBottomNavigationBarItem(
|
||||
icon: Icons.assignment_outlined,
|
||||
activeIcon: Icons.assignment,
|
||||
label: 'Laporan',
|
||||
),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDrawer(BuildContext context) {
|
||||
return Drawer(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppTheme.primaryGradient,
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).padding.top + 16,
|
||||
bottom: 24,
|
||||
left: 16,
|
||||
right: 16),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 40,
|
||||
backgroundColor: Colors.white70,
|
||||
backgroundImage: controller.profilePhotoUrl != null &&
|
||||
controller.profilePhotoUrl!.isNotEmpty
|
||||
? NetworkImage(controller.profilePhotoUrl!)
|
||||
: null,
|
||||
child: controller.profilePhotoUrl == null ||
|
||||
controller.profilePhotoUrl!.isEmpty
|
||||
? const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Halo,',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
controller.nama,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Text(
|
||||
'Donatur',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_on,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
controller.desa ?? 'Tidak ada desa',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: const Text('Profil'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/profile');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.history),
|
||||
title: const Text('Riwayat Donasi'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Implementasi riwayat donasi
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings_outlined),
|
||||
title: const Text('Pengaturan'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Implementasi pengaturan
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: const Text('Keluar'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.logout();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user