h-1 lebaran

This commit is contained in:
Khafidh Fuadi
2025-03-30 14:45:16 +07:00
parent c008020705
commit 5aaeb58d2b
91 changed files with 9448 additions and 3756 deletions

View File

@ -0,0 +1,8 @@
import 'package:get/get.dart';
class AboutBinding extends Bindings {
@override
void dependencies() {
// Tidak perlu controller khusus untuk halaman Tentang Kami
}
}

View File

@ -0,0 +1,454 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class AboutView extends StatelessWidget {
const AboutView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Tentang Kami'),
centerTitle: true,
elevation: 0,
),
body: SingleChildScrollView(
child: Column(
children: [
// Header dengan logo dan nama brand
Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
),
padding: const EdgeInsets.fromLTRB(20, 20, 20, 40),
child: Column(
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Center(
child: Image.asset(
'assets/images/logo-disalurkita.png',
width: 100,
height: 100,
),
),
),
const SizedBox(height: 20),
const Text(
'DisalurKita',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 8),
const Text(
'Salurkan dengan Pasti, Pantau dengan Bukti',
style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 20),
_buildVersionInfo(),
],
),
),
// Konten utama
Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSection(
icon: Icons.info_outline,
title: 'Tentang DisalurKita',
content:
'DisalurKita adalah platform penyaluran bantuan digital yang mengedepankan transparansi, akuntabilitas, dan kemudahan pengelolaan bantuan. Aplikasi ini memudahkan koordinasi antara petugas desa, donatur, dan warga dalam proses penyaluran bantuan sosial.',
),
_buildSection(
icon: Icons.visibility_outlined,
title: 'Visi Kami',
content:
'Menjadi platform terdepan dalam penyaluran bantuan sosial yang transparan, akuntabel, dan berdampak nyata bagi masyarakat Indonesia.',
),
_buildSection(
icon: Icons.location_on_outlined,
title: 'Misi Kami',
content:
'• Memastikan setiap bantuan diterima oleh yang berhak\n• Meningkatkan transparansi dalam proses penyaluran\n• Memberikan kemudahan akses informasi bagi semua pihak\n• Membangun kepercayaan antara donatur dan penerima bantuan',
),
_buildSection(
icon: Icons.star_outline,
title: 'Nilai-nilai Kami',
content:
'• Transparansi: Keterbukaan dalam setiap proses\n• Akuntabilitas: Pertanggungjawaban yang jelas\n• Inklusivitas: Melibatkan semua pihak\n• Efisiensi: Penyaluran bantuan tepat sasaran\n• Inovasi: Terus berinovasi untuk solusi terbaik',
),
_buildSection(
icon: Icons.people_outline,
title: 'Tim Kami',
content:
'DisalurKita dikembangkan oleh tim yang berdedikasi untuk menciptakan solusi inovatif dalam penyaluran bantuan sosial di Indonesia. Tim kami terdiri dari para profesional di bidang teknologi dan pengembangan sosial.',
),
// Layanan
_buildTeamSection(),
// Hubungi Kami
_buildContactSection(),
],
),
),
],
),
),
);
}
Widget _buildVersionInfo() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'Versi 1.0.0',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
);
}
Widget _buildSection({
required IconData icon,
required String title,
required String content,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
color: AppTheme.primaryColor,
size: 24,
),
),
const SizedBox(width: 12),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
],
),
const SizedBox(height: 12),
Text(
content,
style: TextStyle(
fontSize: 15,
color: Colors.grey[700],
height: 1.5,
),
),
],
),
);
}
Widget _buildTeamSection() {
return Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.settings_outlined,
color: AppTheme.primaryColor,
size: 24,
),
),
const SizedBox(width: 12),
const Text(
'Layanan Kami',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
],
),
const SizedBox(height: 16),
// Layanan Grid
GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 0.8,
children: [
_buildServiceItem(
icon: Icons.volunteer_activism_outlined,
title: 'Penitipan Bantuan',
),
_buildServiceItem(
icon: Icons.inventory_2_outlined,
title: 'Pengelolaan Stok',
),
_buildServiceItem(
icon: Icons.local_shipping_outlined,
title: 'Penyaluran Bantuan',
),
_buildServiceItem(
icon: Icons.people_outline,
title: 'Manajemen Penerima',
),
_buildServiceItem(
icon: Icons.assignment_outlined,
title: 'Laporan Transparan',
),
_buildServiceItem(
icon: Icons.campaign_outlined,
title: 'Pengaduan',
),
],
),
],
),
);
}
Widget _buildServiceItem({
required IconData icon,
required String title,
}) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: AppTheme.primaryColor,
size: 28,
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _buildContactSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.contact_mail_outlined,
color: AppTheme.primaryColor,
size: 24,
),
),
const SizedBox(width: 12),
const Text(
'Hubungi Kami',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
],
),
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade50, Colors.white],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
_buildContactItem(
icon: Icons.email_outlined,
title: 'Email',
content: 'info@disalurkita.id',
),
const Divider(height: 24),
_buildContactItem(
icon: Icons.phone_outlined,
title: 'Telepon',
content: '+62 8123 4567 890',
),
const Divider(height: 24),
_buildContactItem(
icon: Icons.location_on_outlined,
title: 'Alamat',
content: 'Jl. Transparansi No. 123, Jakarta Pusat, Indonesia',
),
],
),
),
const SizedBox(height: 20),
// Footer
Center(
child: Text(
'© ${DateTime.now().year} DisalurKita. Seluruh hak cipta dilindungi.',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 20),
],
);
}
Widget _buildContactItem({
required IconData icon,
required String title,
required String content,
}) {
return Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: AppTheme.primaryColor,
size: 20,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
const SizedBox(height: 4),
Text(
content,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
],
),
),
],
);
}
}

View File

@ -50,6 +50,10 @@ class AuthController extends GetxController {
final RxBool isLoading = false.obs;
final RxBool isWargaProfileComplete = false.obs;
// Variable untuk mengontrol visibility password
final RxBool isPasswordHidden = true.obs;
final RxBool isConfirmPasswordHidden = true.obs;
// Flag untuk menandai apakah sudah melakukan pengambilan data profil
final RxBool _hasLoadedProfile = false.obs;
@ -376,6 +380,65 @@ class AuthController extends GetxController {
return null;
}
// Metode untuk reset password
Future<void> resetPassword(String email) async {
if (email.isEmpty) {
Get.snackbar(
'Error',
'Email tidak boleh kosong',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
if (!GetUtils.isEmail(email)) {
Get.snackbar(
'Error',
'Email tidak valid',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
try {
isLoading.value = true;
// Menggunakan Supabase untuk reset password
await _authProvider.resetPasswordForEmail(
email,
redirectTo: 'penyaluranapp://reset-password',
);
Get.snackbar(
'Sukses',
'Instruksi reset password telah dikirim ke email Anda',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
// Kembali ke halaman login
Future.delayed(const Duration(seconds: 3), () {
Get.offNamed(Routes.login);
});
} catch (e) {
print('Error saat reset password: $e');
Get.snackbar(
'Error',
'Terjadi kesalahan saat mengirim reset password. Silakan coba lagi nanti.',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
// Metode untuk refresh data user setelah update profil
Future<void> refreshUserData() async {
try {
@ -543,4 +606,14 @@ class AuthController extends GetxController {
noHpController.clear();
jenisController.clear();
}
// Metode untuk toggle visibility password
void togglePasswordVisibility() {
isPasswordHidden.value = !isPasswordHidden.value;
}
// Metode untuk toggle visibility konfirmasi password
void toggleConfirmPasswordVisibility() {
isConfirmPasswordHidden.value = !isConfirmPasswordHidden.value;
}
}

View File

@ -0,0 +1,269 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
class ForgotPasswordView extends GetView<AuthController> {
const ForgotPasswordView({super.key});
@override
Widget build(BuildContext context) {
final TextEditingController emailController = TextEditingController();
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
return Scaffold(
appBar: AppBar(
title: const Text(
'Lupa Password',
style: TextStyle(color: Color(0xFF1565C0)),
),
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(
Icons.arrow_back,
color: Color(0xFF1565C0),
),
onPressed: () => Get.back(),
),
),
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFE3F2FD), Colors.white],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: SingleChildScrollView(
child: Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 40),
// Logo
Center(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.2),
spreadRadius: 5,
blurRadius: 10,
),
],
),
padding: const EdgeInsets.all(15),
child: const Icon(
Icons.lock_reset,
size: 70,
color: Colors.blue,
),
),
),
const SizedBox(height: 25),
// Judul
const Center(
child: Text(
'Reset Password',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Color(0xFF1565C0),
letterSpacing: 1.2,
),
),
),
const SizedBox(height: 10),
const Center(
child: Text(
'Masukkan email Anda untuk menerima instruksi reset password',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Color(0xFF546E7A),
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 40),
// Email Field
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
spreadRadius: 1,
),
],
),
child: TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'Masukkan email Anda',
labelText: 'Email',
prefixIcon:
const Icon(Icons.email, color: Color(0xFF1565C0)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(
color: Color(0xFF1565C0), width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide:
const BorderSide(color: Colors.red, width: 1.5),
),
fillColor: Colors.white,
filled: true,
),
validator: controller.validateEmail,
),
),
const SizedBox(height: 30),
// Reset Button
Obx(() => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: const Color(0xFF1565C0).withOpacity(0.3),
blurRadius: 10,
spreadRadius: 1,
offset: const Offset(0, 4),
),
],
),
child: ElevatedButton(
onPressed: controller.isLoading.value
? null
: () {
if (formKey.currentState!.validate()) {
controller.resetPassword(
emailController.text.trim());
}
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
backgroundColor: const Color(0xFF1565C0),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 0,
),
child: controller.isLoading.value
? const SpinKitThreeBounce(
color: Colors.white,
size: 24,
)
: const Text(
'KIRIM INSTRUKSI RESET',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
),
)),
const SizedBox(height: 30),
// Kembali ke halaman login
TextButton(
onPressed: () => Get.offNamed(Routes.login),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF1565C0),
),
child: const Text(
'Kembali ke Halaman Login',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
const SizedBox(height: 40),
// Informasi Tambahan
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF1F8E9),
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: const Color(0xFFAED581), width: 1),
),
child: Column(
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Color(0xFFAED581),
shape: BoxShape.circle,
),
child: const Icon(
Icons.info_outline,
color: Color(0xFF33691E),
size: 24,
),
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Informasi Penting',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF33691E),
),
),
),
],
),
const SizedBox(height: 12),
const Text(
'Petunjuk reset password akan dikirim ke email Anda. Silakan periksa kotak masuk atau folder spam setelah permintaan reset password.',
style: TextStyle(
fontSize: 14,
color: Color(0xFF424242),
height: 1.5,
),
),
],
),
),
],
),
),
),
),
),
),
);
}
}

View File

@ -10,140 +10,360 @@ class LoginView extends GetView<AuthController> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: SingleChildScrollView(
child: Form(
key: controller.loginFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 50),
// Logo atau Judul
const Center(
child: Text(
'Penyaluran App',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.blue,
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFE3F2FD), Colors.white],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: SingleChildScrollView(
child: Form(
key: controller.loginFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Logo
Center(
child: Image.asset(
'assets/images/logo-disalurkita.png',
width: 250,
height: 250,
),
),
),
const SizedBox(height: 10),
const Center(
child: Text(
'Masuk ke akun Anda',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
const Center(
child: Text(
'Masuk ke akun Anda',
style: TextStyle(
fontSize: 16,
color: Color(0xFF546E7A),
fontWeight: FontWeight.w500,
),
),
),
),
const SizedBox(height: 50),
const SizedBox(height: 20),
// Email Field
TextFormField(
controller: controller.emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'Email',
prefixIcon: const Icon(Icons.email),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
// Email Field
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
spreadRadius: 1,
),
],
),
child: TextFormField(
controller: controller.emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'Masukkan email Anda',
labelText: 'Email',
prefixIcon:
const Icon(Icons.email, color: Color(0xFF1565C0)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(
color: Color(0xFF1565C0), width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide:
const BorderSide(color: Colors.red, width: 1.5),
),
fillColor: Colors.white,
filled: true,
),
validator: controller.validateEmail,
),
),
validator: controller.validateEmail,
),
const SizedBox(height: 20),
const SizedBox(height: 20),
// Password Field
TextFormField(
controller: controller.passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: const Icon(Icons.lock),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
// Password Field
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
spreadRadius: 1,
),
],
),
child: Obx(() => TextFormField(
controller: controller.passwordController,
obscureText: controller.isPasswordHidden.value,
decoration: InputDecoration(
hintText: 'Masukkan password Anda',
labelText: 'Password',
prefixIcon: const Icon(Icons.lock,
color: Color(0xFF1565C0)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(
color: Color(0xFF1565C0), width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(
color: Colors.red, width: 1.5),
),
fillColor: Colors.white,
filled: true,
suffixIcon: IconButton(
onPressed: () {
controller.isPasswordHidden.value =
!controller.isPasswordHidden.value;
},
icon: Icon(
!controller.isPasswordHidden.value
? Icons.visibility
: Icons.visibility_off,
color: const Color(0xFF78909C),
),
splashRadius: 20,
),
),
validator: controller.validatePassword,
)),
),
validator: controller.validatePassword,
),
const SizedBox(height: 10),
const SizedBox(height: 10),
// Forgot Password
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
// Implementasi lupa password
},
child: const Text('Lupa Password?'),
),
),
const SizedBox(height: 20),
// Login Button
Obx(() => ElevatedButton(
onPressed: controller.isLoading.value
? null
: controller.login,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
// Forgot Password
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => Get.toNamed(Routes.forgotPassword),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF1565C0),
),
child: const Text(
'Lupa Password?',
style: TextStyle(
fontWeight: FontWeight.w600,
),
),
child: controller.isLoading.value
? const SpinKitThreeBounce(
color: Colors.white,
size: 24,
)
: const Text(
'MASUK',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
// Login Button
Obx(() => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: const Color(0xFF1565C0).withOpacity(0.3),
blurRadius: 10,
spreadRadius: 1,
offset: const Offset(0, 4),
),
],
),
child: ElevatedButton(
onPressed: controller.isLoading.value
? null
: controller.login,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
backgroundColor: const Color(0xFF1565C0),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 0,
),
child: controller.isLoading.value
? const SpinKitThreeBounce(
color: Colors.white,
size: 24,
)
: const Text(
'MASUK',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
),
)),
const SizedBox(height: 30),
// Divider
Row(
children: [
Expanded(
child: Container(
height: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.grey.withOpacity(0.1),
Colors.grey.withOpacity(0.5),
],
begin: Alignment.centerRight,
end: Alignment.centerLeft,
),
),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
'ATAU',
style: TextStyle(
color: Color(0xFF546E7A),
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
Expanded(
child: Container(
height: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.grey.withOpacity(0.1),
Colors.grey.withOpacity(0.5),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
),
),
],
),
const SizedBox(height: 30),
// Register Donatur Button
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
spreadRadius: 1,
),
],
),
child: OutlinedButton(
onPressed: () => Get.toNamed(Routes.registerDonatur),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
foregroundColor: const Color(0xFF1565C0),
side: const BorderSide(
color: Color(0xFF1565C0), width: 1.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: const Text(
'DAFTAR SEBAGAI DONATUR',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
),
),
const SizedBox(height: 40),
// Informasi Pendaftaran Warga
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFFF8E1),
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: const Color(0xFFFFCC80), width: 1),
),
child: Column(
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Color(0xFFFFCC80),
shape: BoxShape.circle,
),
child: const Icon(
Icons.info_outline,
color: Color(0xFFE65100),
size: 24,
),
),
)),
const SizedBox(height: 20),
// Divider
const Row(
children: [
Expanded(child: Divider()),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child:
Text('ATAU', style: TextStyle(color: Colors.grey)),
),
Expanded(child: Divider()),
],
),
const SizedBox(height: 20),
// Register Donatur Button
OutlinedButton(
onPressed: () => Get.toNamed(Routes.registerDonatur),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
side: const BorderSide(color: Colors.blue),
),
child: const Text(
'DAFTAR SEBAGAI DONATUR',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
const SizedBox(width: 12),
const Expanded(
child: Text(
'Informasi Penting',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFFE65100),
),
),
),
],
),
const SizedBox(height: 12),
const Text(
'Pendaftaran warga hanya dapat dilakukan melalui aplikasi verifikasi data warga. Silahkan hubungi petugas atau kunjungi kantor untuk informasi lebih lanjut.',
style: TextStyle(
fontSize: 14,
color: Color(0xFF424242),
height: 1.5,
),
),
],
),
),
),
],
const SizedBox(height: 30),
// Footer
Center(
child: Text(
'© ${DateTime.now().year} DisalurKita',
style: TextStyle(
fontSize: 12,
color: Color(0xFF90A4AE),
),
),
),
],
),
),
),
),

View File

@ -11,8 +11,16 @@ class RegisterDonaturView extends GetView<AuthController> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Daftar Sebagai Donatur'),
title: const Text('Daftar Donatur'),
centerTitle: true,
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
),
body: SafeArea(
child: Padding(
@ -23,29 +31,60 @@ class RegisterDonaturView extends GetView<AuthController> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 20),
// Logo atau Judul
const Center(
child: Text(
'Daftar Donatur',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
),
const SizedBox(height: 10),
const Center(
child: Text(
'Isi data untuk mendaftar sebagai donatur',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
// Header dengan icon dan judul
Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(15),
),
child: Column(
children: [
Image.asset(
'assets/images/logo-disalurkita.png',
width: 120,
height: 120,
),
const Text(
'Daftar Sebagai Donatur',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const SizedBox(height: 4),
const Text(
'Bergabunglah dengan kami untuk membantu mereka yang membutuhkan',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.blueGrey,
),
),
],
),
),
const SizedBox(height: 30),
const SizedBox(height: 20),
// Step indicator
const Row(
children: [
Icon(Icons.person_add, color: Colors.blue),
SizedBox(width: 10),
Text(
'Informasi Akun',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
const SizedBox(height: 15),
const Divider(),
const SizedBox(height: 10),
// Nama Lengkap
TextFormField(
@ -53,9 +92,22 @@ class RegisterDonaturView extends GetView<AuthController> {
keyboardType: TextInputType.name,
decoration: InputDecoration(
labelText: 'Nama Lengkap',
prefixIcon: const Icon(Icons.person),
border: OutlineInputBorder(
hintText: 'Masukkan nama lengkap Anda',
prefixIcon: const Icon(Icons.person, color: Colors.blue),
filled: true,
fillColor: Colors.grey.shade100,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
),
),
validator: controller.validateDonaturNama,
@ -68,9 +120,22 @@ class RegisterDonaturView extends GetView<AuthController> {
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'Email',
prefixIcon: const Icon(Icons.email),
border: OutlineInputBorder(
hintText: 'contoh@email.com',
prefixIcon: const Icon(Icons.email, color: Colors.blue),
filled: true,
fillColor: Colors.grey.shade100,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
),
),
validator: controller.validateEmail,
@ -78,34 +143,101 @@ class RegisterDonaturView extends GetView<AuthController> {
const SizedBox(height: 15),
// Password
TextFormField(
controller: controller.passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: const Icon(Icons.lock),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
validator: controller.validatePassword,
),
Obx(() => TextFormField(
controller: controller.passwordController,
obscureText: controller.isPasswordHidden.value,
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Minimal 8 karakter',
prefixIcon:
const Icon(Icons.lock, color: Colors.blue),
suffixIcon: IconButton(
icon: Icon(
controller.isPasswordHidden.value
? Icons.visibility_off
: Icons.visibility,
color: Colors.blue,
),
onPressed: () =>
controller.togglePasswordVisibility(),
),
filled: true,
fillColor: Colors.grey.shade100,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
),
),
validator: controller.validatePassword,
)),
const SizedBox(height: 15),
// Confirm Password
TextFormField(
controller: controller.confirmPasswordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Konfirmasi Password',
prefixIcon: const Icon(Icons.lock_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
Obx(() => TextFormField(
controller: controller.confirmPasswordController,
obscureText: controller.isConfirmPasswordHidden.value,
decoration: InputDecoration(
labelText: 'Konfirmasi Password',
hintText: 'Masukkan password yang sama',
prefixIcon: const Icon(Icons.lock_outline,
color: Colors.blue),
suffixIcon: IconButton(
icon: Icon(
controller.isConfirmPasswordHidden.value
? Icons.visibility_off
: Icons.visibility,
color: Colors.blue,
),
onPressed: () =>
controller.toggleConfirmPasswordVisibility(),
),
filled: true,
fillColor: Colors.grey.shade100,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
),
),
validator: controller.validateConfirmPassword,
)),
const SizedBox(height: 15),
// Section heading
const Row(
children: [
Icon(Icons.person_pin_circle, color: Colors.blue),
SizedBox(width: 10),
Text(
'Informasi Profil',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
),
validator: controller.validateConfirmPassword,
],
),
const SizedBox(height: 15),
const Divider(),
const SizedBox(height: 10),
// No HP
TextFormField(
@ -113,9 +245,22 @@ class RegisterDonaturView extends GetView<AuthController> {
keyboardType: TextInputType.phone,
decoration: InputDecoration(
labelText: 'Nomor HP',
prefixIcon: const Icon(Icons.phone),
border: OutlineInputBorder(
hintText: 'Masukkan nomor HP aktif',
prefixIcon: const Icon(Icons.phone, color: Colors.blue),
filled: true,
fillColor: Colors.grey.shade100,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
),
),
validator: controller.validateDonaturNoHp,
@ -128,10 +273,23 @@ class RegisterDonaturView extends GetView<AuthController> {
keyboardType: TextInputType.streetAddress,
maxLines: 2,
decoration: InputDecoration(
labelText: 'Alamat',
prefixIcon: const Icon(Icons.home),
border: OutlineInputBorder(
labelText: 'Alamat Lengkap',
hintText: 'Masukkan alamat lengkap Anda',
prefixIcon: const Icon(Icons.home, color: Colors.blue),
filled: true,
fillColor: Colors.grey.shade100,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
),
),
validator: controller.validateDonaturAlamat,
@ -139,70 +297,160 @@ class RegisterDonaturView extends GetView<AuthController> {
const SizedBox(height: 15),
// Jenis Donatur (Dropdown)
DropdownButtonFormField<String>(
value: controller.jenisController.text.isEmpty
? 'Individu'
: controller.jenisController.text,
decoration: InputDecoration(
labelText: 'Jenis Donatur',
prefixIcon: const Icon(Icons.category),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade300),
),
child: DropdownButtonFormField<String>(
value: controller.jenisController.text.isEmpty
? 'Individu'
: controller.jenisController.text,
decoration: InputDecoration(
labelText: 'Jenis Donatur',
prefixIcon:
const Icon(Icons.category, color: Colors.blue),
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 10),
),
items: const [
DropdownMenuItem(
value: 'Individu', child: Text('Individu')),
DropdownMenuItem(
value: 'Organisasi', child: Text('Organisasi')),
DropdownMenuItem(
value: 'Perusahaan', child: Text('Perusahaan')),
DropdownMenuItem(
value: 'Lainnya', child: Text('Lainnya')),
],
onChanged: (value) {
controller.jenisController.text = value ?? 'Individu';
},
),
items: const [
DropdownMenuItem(
value: 'Individu', child: Text('Individu')),
DropdownMenuItem(
value: 'Organisasi', child: Text('Organisasi')),
DropdownMenuItem(
value: 'Perusahaan', child: Text('Perusahaan')),
DropdownMenuItem(
value: 'Lainnya', child: Text('Lainnya')),
],
onChanged: (value) {
controller.jenisController.text = value ?? 'Individu';
},
),
const SizedBox(height: 15),
// Register Button
Obx(() => ElevatedButton(
onPressed: controller.isLoading.value
? null
: controller.registerDonatur,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: controller.isLoading.value
? const SpinKitThreeBounce(
color: Colors.white,
size: 24,
)
: const Text(
'DAFTAR',
const SizedBox(height: 25),
// Catatan Informasi
Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.blue.shade200),
),
child: Row(
children: [
const Icon(Icons.info_outline, color: Colors.blue),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Informasi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
SizedBox(height: 5),
Text(
'Data Anda akan terverifikasi dan terlindungi. Kami menjaga privasi dan keamanan data Anda.',
style: TextStyle(
fontSize: 14,
color: Colors.blueGrey,
),
),
],
),
),
],
),
),
const SizedBox(height: 25),
// Register Button
Obx(() => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 2),
),
],
),
child: ElevatedButton(
onPressed: controller.isLoading.value
? null
: controller.registerDonatur,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 0,
),
child: controller.isLoading.value
? const SpinKitThreeBounce(
color: Colors.white,
size: 24,
)
: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.how_to_reg, color: Colors.white),
SizedBox(width: 10),
Text(
'DAFTAR SEKARANG',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
)),
const SizedBox(height: 20),
// Login Link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Sudah punya akun?'),
TextButton(
onPressed: () => Get.offAllNamed(Routes.login),
child: const Text('Masuk'),
),
],
Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade200),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Sudah punya akun?',
style: TextStyle(color: Colors.grey),
),
TextButton(
onPressed: () => Get.offAllNamed(Routes.login),
child: const Text(
'Masuk',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
),
],
),
),
const SizedBox(height: 20),
],
),
),

View File

@ -7,6 +7,7 @@ import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/data/models/laporan_penyaluran_model.dart';
import 'package:penyaluran_app/app/data/models/user_model.dart';
import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart';
import 'package:penyaluran_app/app/data/models/lokasi_penyaluran_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
@ -45,6 +46,10 @@ class DonaturDashboardController extends GetxController {
// Data untuk stok bantuan yang tersedia
final RxList<StokBantuanModel> stokBantuan = <StokBantuanModel>[].obs;
// Data untuk lokasi penyaluran
final RxList<LokasiPenyaluranModel> lokasiPenyaluran =
<LokasiPenyaluranModel>[].obs;
// Indikator loading
final RxBool isLoading = false.obs;
@ -199,6 +204,9 @@ class DonaturDashboardController extends GetxController {
// Ambil data stok bantuan
await fetchStokBantuan();
// Ambil data lokasi penyaluran
await fetchLokasiPenyaluran();
// Ambil data notifikasi
await fetchNotifikasi();
} catch (e) {
@ -233,7 +241,7 @@ class DonaturDashboardController extends GetxController {
.from('penyaluran_bantuan')
.select(
'*, lokasi_penyaluran:lokasi_penyaluran_id(*), kategori:kategori_bantuan_id(*), petugas:petugas_id(*)')
.order('tanggal_penyaluran', ascending: true);
.order('tanggal_penyaluran', ascending: false);
// Konversi ke model lalu filter di sisi client
final allJadwal = response
@ -243,9 +251,7 @@ class DonaturDashboardController extends GetxController {
// Filter jadwal yang tanggalnya lebih besar dari hari ini
jadwalPenyaluran.value = allJadwal
.where((jadwal) =>
jadwal.tanggalPenyaluran != null &&
jadwal.tanggalPenyaluran!.isAfter(now))
.where((jadwal) => jadwal.tanggalPenyaluran != null)
.toList();
} catch (e) {
print('Error fetching jadwal penyaluran: $e');
@ -306,6 +312,23 @@ class DonaturDashboardController extends GetxController {
}
}
// Ambil data lokasi penyaluran
Future<void> fetchLokasiPenyaluran() async {
try {
final response = await _supabaseService.client
.from('lokasi_penyaluran')
.select()
.eq('is_lokasi_titip', true)
.order('nama');
lokasiPenyaluran.value = (response as List<dynamic>)
.map((data) => LokasiPenyaluranModel.fromJson(data))
.toList();
} catch (e) {
print('Error fetching lokasi penyaluran: $e');
}
}
// Ambil data notifikasi
Future<void> fetchNotifikasi() async {
try {
@ -386,6 +409,7 @@ class DonaturDashboardController extends GetxController {
double jumlah,
String deskripsi,
String? skemaBantuanId,
String? lokasiPenyaluranId,
) async {
try {
isLoading.value = true;
@ -426,15 +450,25 @@ class DonaturDashboardController extends GetxController {
'tanggal_penitipan': DateTime.now().toIso8601String(),
'foto_bantuan': fotoBantuanUrls,
'is_uang': selectedStokBantuan.isUang ?? false,
'skema_bantuan_id': skemaBantuanId,
'lokasi_penyaluran_id': lokasiPenyaluranId,
};
// Tambahkan skema bantuan jika ada
if (skemaBantuanId != null && skemaBantuanId.isNotEmpty) {
data['skema_bantuan_id'] = skemaBantuanId;
}
// Simpan ke database
await _supabaseService.client.from('penitipan_bantuan').insert(data);
final response = await _supabaseService.client
.from('penitipan_bantuan')
.insert(data)
.select('id')
.single();
// Tampilkan pesan sukses
Get.snackbar(
'Berhasil',
'Penitipan bantuan berhasil diinput',
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
// Reset foto bantuan setelah berhasil disimpan
resetFotoBantuan();
@ -442,19 +476,13 @@ class DonaturDashboardController extends GetxController {
// Ambil data penitipan bantuan yang baru
await fetchPenitipanBantuan();
// Tampilkan pesan sukses
Get.snackbar(
'Berhasil',
'Penitipan bantuan berhasil dikirim dan akan diproses oleh petugas desa',
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
// Kembali ke halaman utama
Get.back();
} catch (e) {
print('Error creating penitipan bantuan: $e');
Get.snackbar(
'Gagal',
'Terjadi kesalahan saat mengirim penitipan bantuan: $e',
'Terjadi kesalahan: $e',
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 3),
@ -475,7 +503,7 @@ class DonaturDashboardController extends GetxController {
.eq('id', lokasiId)
.single();
if (response != null && response['nama'] != null) {
if (response['nama'] != null) {
return response['nama'] as String;
}
return null;

View File

@ -1,8 +1,8 @@
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/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart';
class DonaturDashboardView extends GetView<DonaturDashboardController> {
@ -36,13 +36,57 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header DisalurKita dengan logo dan slogan
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Image.asset(
'assets/images/logo-disalurkita.png',
width: 50,
height: 50,
),
const SizedBox(width: 15),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'DisalurKita',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF1565C0),
),
),
SizedBox(height: 5),
Text(
'Salurkan dengan Pasti, Pantau dengan Bukti',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
_buildWelcomeSection(),
const SizedBox(height: 24),
_buildStatisticSection(),
const SizedBox(height: 24),
_buildUpcomingEvents(),
const SizedBox(height: 24),
_buildRecentPenitipan(),
],
),
),
@ -101,14 +145,24 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
child: CircleAvatar(
radius: 30,
backgroundColor: Colors.blue.shade100,
backgroundImage: controller.profilePhotoUrl != null
backgroundImage: controller.profilePhotoUrl != null &&
controller.profilePhotoUrl!.isNotEmpty
? NetworkImage(controller.profilePhotoUrl!)
: null,
child: controller.profilePhotoUrl == null
? Icon(
Icons.person,
color: Colors.blue.shade700,
size: 30,
child: (controller.profilePhotoUrl == null ||
controller.profilePhotoUrl!.isEmpty)
? Text(
controller.nama.isNotEmpty
? controller.nama
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
fontSize: 24,
),
)
: null,
),
@ -263,7 +317,7 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
child: _buildStatCard(
title: 'Diterima',
value:
'${controller.penitipanBantuan.where((p) => p.status == 'DITERIMA').length}',
'${controller.penitipanBantuan.where((p) => p.status == 'TERVERIFIKASI').length}',
icon: Icons.check_circle_outline,
color: Colors.green,
),
@ -284,125 +338,6 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
);
}
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,
@ -545,7 +480,7 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
Widget _buildEventCard(dynamic event) {
final formattedDate = event.tanggalPenyaluran != null
? DateFormat('dd MMMM yyyy', 'id_ID').format(event.tanggalPenyaluran!)
? FormatHelper.formatDateTime(event.tanggalPenyaluran!)
: 'Tanggal tidak tersedia';
return Container(
@ -588,7 +523,8 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
),
Text(
event.tanggalPenyaluran != null
? DateFormat('dd').format(event.tanggalPenyaluran!)
? FormatHelper.formatDateTime(
event.tanggalPenyaluran!)
: '--',
style: TextStyle(
fontSize: 14,
@ -640,8 +576,7 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
Widget _buildPenitipanCard(dynamic penitipan) {
final formattedDate = penitipan.tanggalPenitipan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(penitipan.tanggalPenitipan!)
? FormatHelper.formatDateTime(penitipan.tanggalPenitipan!)
: 'Tanggal tidak tersedia';
Color statusColor;

View File

@ -1,12 +1,12 @@
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/data/models/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
const DonaturJadwalDetailView({Key? key}) : super(key: key);
const DonaturJadwalDetailView({super.key});
@override
DonaturDashboardController get controller {
@ -35,7 +35,6 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
_buildDetailSection(jadwal),
_buildPelaksanaSection(jadwal),
_buildStatusSection(jadwal),
_buildActionSection(jadwal),
],
),
),
@ -126,8 +125,7 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
),
const SizedBox(width: 8),
Text(
DateFormat('EEEE, dd MMMM yyyy', 'id_ID')
.format(jadwal.tanggalPenyaluran!),
FormatHelper.formatDateIndonesian(jadwal.tanggalPenyaluran),
style: const TextStyle(
fontSize: 16,
color: Colors.white,
@ -204,11 +202,26 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
CircleAvatar(
radius: 25,
backgroundColor: Colors.blue.shade100,
child: Icon(
Icons.person,
color: Colors.blue.shade700,
size: 30,
),
backgroundImage: jadwal.fotoPetugas != null &&
jadwal.fotoPetugas.toString().isNotEmpty
? NetworkImage(jadwal.fotoPetugas as String)
: null,
child: (jadwal.fotoPetugas == null ||
jadwal.fotoPetugas.toString().isEmpty)
? Text(
jadwal.namaPetugas != null
? jadwal.namaPetugas
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
fontSize: 20,
),
)
: null,
),
const SizedBox(width: 16),
Expanded(
@ -254,50 +267,87 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
children: [
const SectionHeader(title: 'Status Penyaluran'),
const SizedBox(height: 16),
_buildStatusTimeline(jadwal),
_buildStatusCard(jadwal),
],
),
);
}
Widget _buildStatusTimeline(PenyaluranBantuanModel jadwal) {
Widget _buildStatusCard(PenyaluranBantuanModel jadwal) {
final status = jadwal.status;
final bool isCompleted = status == 'SELESAI';
final bool isCancelled = status == 'DIBATALKAN';
final bool isInProgress = status == 'DALAM_PROSES';
final bool isCompleted = status == 'TERLAKSANA';
final bool isCancelled = status == 'BATALTERLAKSANA';
final bool isInProgress = status == 'AKTIF';
final bool isScheduled = status == 'Dijadwalkan';
Color statusColor = Colors.blue;
IconData statusIcon = Icons.schedule;
String statusText = 'Dijadwalkan';
if (isCompleted) {
statusColor = Colors.green;
statusIcon = Icons.check_circle;
statusText = 'Terlaksana';
} else if (isCancelled) {
statusColor = Colors.red;
statusIcon = Icons.cancel;
statusText = 'Batal Terlaksana';
} else if (isInProgress) {
statusColor = Colors.blue;
statusIcon = Icons.sync;
statusText = 'Aktif';
} else if (isScheduled) {
statusColor = Colors.orange;
statusIcon = Icons.schedule;
statusText = 'Dijadwalkan';
}
return Column(
children: [
_buildTimelineItem(
title: 'Dijadwalkan',
date: jadwal.createdAt != null
? DateFormat('dd MMM yyyy', 'id_ID').format(jadwal.createdAt!)
: '-',
isCompleted: true,
isFirst: true,
),
_buildTimelineItem(
title: 'Dalam Proses',
date: isInProgress || isCompleted
? jadwal.tanggalPenyaluran != null
? DateFormat('dd MMM yyyy', 'id_ID')
.format(jadwal.tanggalPenyaluran!)
: '-'
: '-',
isCompleted: isInProgress || isCompleted,
isCancelled: isCancelled,
),
_buildTimelineItem(
title: 'Selesai',
date: isCompleted
? jadwal.tanggalSelesai != null
? DateFormat('dd MMM yyyy', 'id_ID')
.format(jadwal.tanggalSelesai!)
: '-'
: '-',
isCompleted: isCompleted,
isCancelled: isCancelled,
isLast: true,
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: statusColor.withOpacity(0.3)),
),
child: Column(
children: [
Row(
children: [
Icon(statusIcon, color: statusColor, size: 28),
const SizedBox(width: 12),
Text(
statusText,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: statusColor,
),
),
],
),
const SizedBox(height: 16),
_buildStatusDetailItem(
title: 'Tanggal Dijadwalkan',
value: FormatHelper.formatDateIndonesian(jadwal.createdAt),
),
const SizedBox(height: 8),
_buildStatusDetailItem(
title: 'Tanggal Penyaluran',
value:
FormatHelper.formatDateIndonesian(jadwal.tanggalPenyaluran),
),
if (isCompleted) ...[
const SizedBox(height: 8),
_buildStatusDetailItem(
title: 'Tanggal Selesai',
value:
FormatHelper.formatDateIndonesian(jadwal.tanggalSelesai),
),
],
],
),
),
if (isCancelled) ...[
const SizedBox(height: 16),
@ -333,7 +383,7 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
const SizedBox(height: 8),
if (jadwal.tanggalPembatalan != null)
Text(
'Dibatalkan pada: ${DateFormat('dd MMMM yyyy', 'id_ID').format(jadwal.tanggalPembatalan!)}',
'Dibatalkan pada: ${FormatHelper.formatDateIndonesian(jadwal.tanggalPembatalan)}',
style: TextStyle(
fontSize: 14,
color: Colors.red.shade700,
@ -347,159 +397,29 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
);
}
Widget _buildTimelineItem({
required String title,
required String date,
required bool isCompleted,
bool isFirst = false,
bool isLast = false,
bool isCancelled = false,
}) {
Widget _buildStatusDetailItem(
{required String title, required String value}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: 20,
child: Column(
children: [
if (!isFirst)
Container(
width: 2,
height: 20,
color: isCompleted
? Colors.green
: isCancelled
? Colors.red
: Colors.grey.shade300,
),
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isCompleted
? Colors.green
: isCancelled
? Colors.red
: Colors.grey.shade300,
border: Border.all(
color: isCompleted
? Colors.green
: isCancelled
? Colors.red
: Colors.grey.shade300,
width: 2,
),
),
child: isCompleted
? const Icon(Icons.check, size: 12, color: Colors.white)
: isCancelled
? const Icon(Icons.close, size: 12, color: Colors.white)
: null,
),
if (!isLast)
Container(
width: 2,
height: 20,
color: isCompleted && !isCancelled
? Colors.green
: Colors.grey.shade300,
),
],
Text(
title,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade700,
),
),
const SizedBox(width: 12),
Expanded(
child: Container(
margin: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isCompleted
? Colors.black
: isCancelled
? Colors.red
: Colors.grey,
),
),
Text(
date,
style: TextStyle(
fontSize: 14,
color: isCompleted
? Colors.grey.shade700
: isCancelled
? Colors.red.shade300
: Colors.grey.shade400,
),
),
],
),
Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
);
}
Widget _buildActionSection(PenyaluranBantuanModel jadwal) {
if (jadwal.status == 'DIBATALKAN') {
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SectionHeader(title: 'Tindakan'),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => _hubungiPetugas(jadwal),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12),
backgroundColor: Colors.green,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(Icons.chat_outlined),
label: const Text('Hubungi Petugas'),
),
),
if (jadwal.status == 'SELESAI') ...[
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: () => _lihatLaporan(jadwal),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12),
foregroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(Icons.description_outlined),
label: const Text('Lihat Laporan'),
),
),
],
],
),
],
),
);
}
Widget _buildInfoItem({
required IconData icon,
required String title,

View File

@ -2,6 +2,7 @@ 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/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart';
class DonaturJadwalView extends GetView<DonaturDashboardController> {
@ -97,7 +98,7 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
for (var jadwal in controller.jadwalPenyaluran) {
if (jadwal.tanggalPenyaluran != null) {
String monthYear =
DateFormat('MMMM yyyy', 'id_ID').format(jadwal.tanggalPenyaluran!);
FormatHelper.formatDate(jadwal.tanggalPenyaluran!, format: 'MMMM');
if (!groupedJadwal.containsKey(monthYear)) {
groupedJadwal[monthYear] = [];
@ -110,9 +111,14 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
// 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);
try {
DateTime dateA = DateFormat('MMMM yyyy', 'id_ID').parse(a);
DateTime dateB = DateFormat('MMMM yyyy', 'id_ID').parse(b);
return dateA.compareTo(dateB);
} catch (e) {
// Fallback sorting jika parse error
return a.compareTo(b);
}
});
return ListView(
@ -158,28 +164,27 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
Widget _buildJadwalCard(dynamic jadwal) {
final formattedDate = jadwal.tanggalPenyaluran != null
? DateFormat('EEEE, dd MMMM yyyy', 'id_ID')
.format(jadwal.tanggalPenyaluran!)
? FormatHelper.formatDateTime(jadwal.tanggalPenyaluran!)
: 'Tanggal tidak tersedia';
String statusText = 'Akan Datang';
String statusText = 'Dijadwalkan';
Color statusColor = Colors.blue;
switch (jadwal.status) {
case 'SELESAI':
statusText = 'Selesai';
case 'TERLAKSANA':
statusText = 'Terlaksana';
statusColor = Colors.green;
break;
case 'DIBATALKAN':
statusText = 'Dibatalkan';
case 'BATALTERLAKSANA':
statusText = 'Batal Terlaksana';
statusColor = Colors.red;
break;
case 'DALAM_PROSES':
statusText = 'Dalam Proses';
statusColor = Colors.orange;
case 'AKTIF':
statusText = 'Aktif';
statusColor = Colors.blue;
break;
default:
statusText = 'Akan Datang';
statusText = 'Dijadwalkan';
statusColor = Colors.blue;
}
@ -248,8 +253,9 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
),
child: Text(
jadwal.tanggalPenyaluran != null
? DateFormat('MMM', 'id_ID')
.format(jadwal.tanggalPenyaluran!)
? FormatHelper.formatDate(
jadwal.tanggalPenyaluran!,
format: 'MMM')
.toUpperCase()
: 'TBD',
style: const TextStyle(
@ -265,8 +271,9 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
child: Center(
child: Text(
jadwal.tanggalPenyaluran != null
? DateFormat('dd')
.format(jadwal.tanggalPenyaluran!)
? FormatHelper.formatDate(
jadwal.tanggalPenyaluran!,
format: 'dd')
: '-',
style: TextStyle(
fontSize: 24,
@ -459,11 +466,11 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
IconData _getStatusIcon(String? status) {
switch (status) {
case 'SELESAI':
case 'TERLAKSANA':
return Icons.check_circle;
case 'DIBATALKAN':
case 'BATALTERLAKSANA':
return Icons.cancel;
case 'DALAM_PROSES':
case 'AKTIF':
return Icons.timelapse;
default:
return Icons.event_available;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,8 @@
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/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
DonaturRiwayatPenitipanView({super.key});
@ -60,8 +61,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
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!)
? FormatHelper.formatDateTime(item.tanggalPenitipan!)
.toLowerCase()
: '';
@ -214,8 +214,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
Widget _buildPenitipanCard(
BuildContext context, dynamic penitipan, Color statusColor) {
final formattedDate = penitipan.tanggalPenitipan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(penitipan.tanggalPenitipan!)
? FormatHelper.formatDateTime(penitipan.tanggalPenitipan!)
: 'Tanggal tidak tersedia';
IconData statusIcon;
@ -435,61 +434,6 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
return id != null ? 'Petugas Desa' : 'Tidak ada petugas';
}
void showFullScreenImage(String imageUrl) {
Get.dialog(
Dialog(
insetPadding: EdgeInsets.zero,
child: Container(
color: Colors.black,
child: Stack(
fit: StackFit.expand,
children: [
InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 4,
child: Image.network(
imageUrl,
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
),
),
Positioned(
top: 20,
right: 20,
child: GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
color: Colors.white,
size: 24,
),
),
),
),
],
),
),
),
);
}
Get.dialog(
AlertDialog(
title: const Text('Detail Penitipan'),
@ -509,8 +453,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
_buildInfoRow(
'Tanggal Penitipan',
penitipan.tanggalPenitipan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(penitipan.tanggalPenitipan!)
? FormatHelper.formatDateTime(penitipan.tanggalPenitipan!)
: 'Tanggal tidak tersedia',
),
_buildInfoRow(
@ -520,8 +463,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
if (penitipan.tanggalVerifikasi != null)
_buildInfoRow(
'Tanggal Verifikasi',
DateFormat('dd MMMM yyyy HH:mm', 'id_ID')
.format(penitipan.tanggalVerifikasi!),
FormatHelper.formatDateTime(penitipan.tanggalVerifikasi!),
),
if (penitipan.deskripsi != null &&
penitipan.deskripsi!.isNotEmpty)
@ -543,8 +485,10 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
),
const SizedBox(height: 8),
GestureDetector(
onTap: () =>
showFullScreenImage(penitipan.fotoBantuan!.first),
onTap: () => ShowImageDialog.showFullScreen(
context,
penitipan.fotoBantuan!.first,
),
child: Container(
height: 200,
width: double.infinity,
@ -572,8 +516,10 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
),
const SizedBox(height: 8),
GestureDetector(
onTap: () =>
showFullScreenImage(penitipan.fotoBuktiSerahTerima!),
onTap: () => ShowImageDialog.showFullScreen(
context,
penitipan.fotoBuktiSerahTerima!,
),
child: Container(
height: 200,
width: double.infinity,

View File

@ -4,7 +4,6 @@ import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard
import 'package:penyaluran_app/app/widgets/section_header.dart';
import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/utils/date_helper.dart';
class DonaturSkemaView extends GetView<DonaturDashboardController> {
const DonaturSkemaView({super.key});
@ -549,14 +548,14 @@ class DonaturSkemaView extends GetView<DonaturDashboardController> {
int days = difference.inDays;
if (days > 0) {
return 'Batas waktu: ${days} hari lagi';
return 'Batas waktu: $days hari lagi';
} else {
int hours = difference.inHours;
if (hours > 0) {
return 'Batas waktu: ${hours} jam lagi';
return 'Batas waktu: $hours jam lagi';
} else {
int minutes = difference.inMinutes;
return 'Batas waktu: ${minutes} menit lagi';
return 'Batas waktu: $minutes menit lagi';
}
}
}
@ -597,20 +596,20 @@ class DonaturSkemaView extends GetView<DonaturDashboardController> {
}
}
// Format nilai sebagai Rupiah menggunakan DateHelper
return DateHelper.formatRupiah(nilai);
return FormatHelper.formatRupiah(nilai);
}
// Jika bukan uang, kembalikan nilai + satuan (jika ada)
return '${jumlahDiterimaPerOrang} ${stokBantuan.satuan ?? ''}';
return '$jumlahDiterimaPerOrang ${stokBantuan.satuan ?? ''}';
}
String _formatRupiah(dynamic amount) {
if (amount is num) {
return DateHelper.formatRupiah(amount);
return FormatHelper.formatRupiah(amount);
} else if (amount is String) {
try {
double nilai = double.parse(amount);
return DateHelper.formatRupiah(nilai);
return FormatHelper.formatRupiah(nilai);
} catch (e) {
return 'Rp ${amount.replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}';
}

View File

@ -34,7 +34,7 @@ class DonaturView extends GetView<DonaturDashboardController> {
title: Obx(() {
switch (controller.activeTabIndex.value) {
case 0:
return const Text('Dashboard Donatur');
return const Text('Dashboard');
case 1:
return const Text('Skema Bantuan');
case 2:
@ -44,7 +44,7 @@ class DonaturView extends GetView<DonaturDashboardController> {
case 4:
return const Text('Laporan Penyaluran');
default:
return const Text('Dashboard Donatur');
return const Text('Dashboard');
}
}),
leading: IconButton(
@ -201,12 +201,20 @@ class DonaturView extends GetView<DonaturDashboardController> {
controller.profilePhotoUrl!.isNotEmpty
? NetworkImage(controller.profilePhotoUrl!)
: null,
child: controller.profilePhotoUrl == null ||
controller.profilePhotoUrl!.isEmpty
? const Icon(
Icons.person,
color: Colors.white,
size: 40,
child: (controller.profilePhotoUrl == null ||
controller.profilePhotoUrl!.isEmpty)
? Text(
controller.nama.isNotEmpty
? controller.nama
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
fontSize: 24,
),
)
: null,
),
@ -284,44 +292,175 @@ class DonaturView extends GetView<DonaturDashboardController> {
child: ListView(
padding: EdgeInsets.zero,
children: [
ListTile(
leading: const Icon(Icons.person_outline),
title: const Text('Profil'),
_buildMenuCategory('Menu Utama'),
Obx(() => _buildMenuItem(
icon: Icons.dashboard_outlined,
activeIcon: Icons.dashboard,
title: 'Dashboard',
isSelected: controller.activeTabIndex.value == 0,
onTap: () {
Navigator.pop(context);
controller.activeTabIndex.value = 0;
},
)),
Obx(() => _buildMenuItem(
icon: Icons.description_outlined,
activeIcon: Icons.description,
title: 'Skema Bantuan',
isSelected: controller.activeTabIndex.value == 1,
onTap: () {
Navigator.pop(context);
controller.activeTabIndex.value = 1;
},
)),
Obx(() => _buildMenuItem(
icon: Icons.calendar_today_outlined,
activeIcon: Icons.calendar_today,
title: 'Jadwal Penyaluran',
isSelected: controller.activeTabIndex.value == 2,
onTap: () {
Navigator.pop(context);
controller.activeTabIndex.value = 2;
},
)),
Obx(() => _buildMenuItem(
icon: Icons.add_box_outlined,
activeIcon: Icons.add_box,
title: 'Penitipan Bantuan',
isSelected: controller.activeTabIndex.value == 3,
onTap: () {
Navigator.pop(context);
controller.activeTabIndex.value = 3;
},
)),
Obx(() => _buildMenuItem(
icon: Icons.assignment_outlined,
activeIcon: Icons.assignment,
title: 'Laporan Penyaluran',
isSelected: controller.activeTabIndex.value == 4,
onTap: () {
Navigator.pop(context);
controller.activeTabIndex.value = 4;
},
)),
_buildMenuCategory('Pengaturan'),
_buildMenuItem(
icon: Icons.person_outline,
activeIcon: Icons.person,
title: 'Profil',
onTap: () {
Navigator.pop(context);
Get.toNamed('/profile');
},
),
ListTile(
leading: const Icon(Icons.history),
title: const Text('Riwayat Donasi'),
_buildMenuItem(
icon: Icons.info_outline,
activeIcon: Icons.info,
title: 'Tentang Kami',
onTap: () {
Navigator.pop(context);
// TODO: Implementasi riwayat donasi
Get.toNamed('/about');
},
),
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'),
_buildMenuItem(
icon: Icons.logout,
title: 'Keluar',
onTap: () {
Navigator.pop(context);
controller.logout();
},
isLogout: true,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'© ${DateTime.now().year} DisalurKita',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
),
],
),
);
}
Widget _buildMenuCategory(String title) {
return Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
child: Text(
title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
),
);
}
Widget _buildMenuItem({
required IconData icon,
IconData? activeIcon,
required String title,
bool isSelected = false,
String? badge,
required Function() onTap,
bool isLogout = false,
}) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: isSelected
? AppTheme.primaryColor.withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: ListTile(
leading: Icon(
isSelected ? (activeIcon ?? icon) : icon,
color: isSelected
? AppTheme.primaryColor
: (isLogout ? Colors.red : null),
),
title: Text(
title,
style: TextStyle(
color: isSelected
? AppTheme.primaryColor
: (isLogout ? Colors.red : null),
fontWeight: isSelected ? FontWeight.bold : null,
),
),
trailing: badge != null
? Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(10),
),
constraints: const BoxConstraints(
minWidth: 20,
minHeight: 20,
),
child: Text(
badge,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
textAlign: TextAlign.center,
),
)
: null,
onTap: onTap,
),
);
}
}

View File

@ -11,7 +11,6 @@ import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'package:open_file/open_file.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:http/http.dart' as http;
import 'package:penyaluran_app/app/utils/format_helper.dart';
@ -635,7 +634,7 @@ class LaporanPenyaluranController extends GetxController {
fontSize: 12,
color: PdfColors.blue900)),
pw.Text(
'Tanggal: ${DateFormat('dd MMMM yyyy').format(DateTime.now())}',
'Tanggal: ${FormatHelper.formatDateTime(DateTime.now())}',
style: pw.TextStyle(font: ttf, fontSize: 10),
),
],
@ -708,8 +707,7 @@ class LaporanPenyaluranController extends GetxController {
_buildPdfRow(
'Tanggal Laporan',
laporan.tanggalLaporan != null
? DateTimeHelper.formatDateTime(
laporan.tanggalLaporan!)
? FormatHelper.formatDateTime(laporan.tanggalLaporan!)
: '-',
ttf,
ttfBold),
@ -731,7 +729,7 @@ class LaporanPenyaluranController extends GetxController {
_buildPdfRow(
'Tanggal Penyaluran',
penyaluran.tanggalPenyaluran != null
? DateTimeHelper.formatDateTime(
? FormatHelper.formatDateTime(
penyaluran.tanggalPenyaluran!)
: '-',
ttf,
@ -739,7 +737,7 @@ class LaporanPenyaluranController extends GetxController {
_buildPdfRow(
'Tanggal Selesai',
penyaluran.tanggalSelesai != null
? DateTimeHelper.formatDateTime(
? FormatHelper.formatDateTime(
penyaluran.tanggalSelesai!)
: '-',
ttf,
@ -902,7 +900,7 @@ class LaporanPenyaluranController extends GetxController {
final isUang = stokBantuan['is_uang'] == true;
final formattedJumlah = isUang
? 'Rp ${NumberFormat.currency(locale: 'id', symbol: '', decimalDigits: 0).format(jumlah)}'
? FormatHelper.formatRupiah(jumlah)
: '$jumlah ${stokBantuan['satuan'] ?? ''}';
return pw.TableRow(
@ -975,7 +973,7 @@ class LaporanPenyaluranController extends GetxController {
final jumlahBantuan = penerima.jumlahBantuan ?? 0;
final formattedJumlah = isUang
? 'Rp ${NumberFormat.currency(locale: 'id', symbol: '', decimalDigits: 0).format(jumlahBantuan)}'
? FormatHelper.formatRupiah(jumlahBantuan)
: '$jumlahBantuan ${penerima.satuan ?? ''}';
return pw.TableRow(

View File

@ -65,7 +65,7 @@ class LaporanPenyaluranCreateView extends GetView<LaporanPenyaluranController> {
controller.selectedPenyaluran.value!
.tanggalPenyaluran !=
null
? DateTimeHelper.formatDateTime(controller
? FormatHelper.formatDateTime(controller
.selectedPenyaluran.value!.tanggalPenyaluran!)
: '-',
),
@ -73,7 +73,7 @@ class LaporanPenyaluranCreateView extends GetView<LaporanPenyaluranController> {
'Tanggal Selesai',
controller.selectedPenyaluran.value!.tanggalSelesai !=
null
? DateTimeHelper.formatDateTime(controller
? FormatHelper.formatDateTime(controller
.selectedPenyaluran.value!.tanggalSelesai!)
: '-',
),

View File

@ -6,7 +6,6 @@ import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/custom_app_bar.dart';
import 'package:penyaluran_app/app/widgets/status_badge.dart';
import 'package:intl/intl.dart';
class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
const LaporanPenyaluranView({super.key});
@ -255,8 +254,8 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
StatusBadge(status: laporan.status ?? 'DRAFT'),
// const SizedBox(width: 8),
// StatusBadge(status: laporan.status ?? 'DRAFT'),
],
),
),
@ -273,10 +272,11 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
Icons.calendar_today,
'Tanggal',
laporan.tanggalLaporan != null
? DateTimeHelper.formatDateTime(
? FormatHelper.formatDateTime(
laporan.tanggalLaporan!)
: '-',
),
const SizedBox(width: 16),
_buildInfoItem(
Icons.description,
'Status',
@ -538,8 +538,8 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
const SizedBox(width: 4),
Text(
penyaluran.tanggalSelesai != null
? DateFormat('dd/MM/yyyy')
.format(penyaluran.tanggalSelesai!)
? FormatHelper.formatDateTime(
penyaluran.tanggalSelesai!)
: '-',
style: TextStyle(
fontSize: 12,

View File

@ -0,0 +1,20 @@
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
class JadwalPenyaluranBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<JadwalPenyaluranController>(
() => JadwalPenyaluranController(),
);
// Register service untuk komunikasi pembaruan jadwal
if (!Get.isRegistered<JadwalUpdateService>()) {
Get.lazyPut<JadwalUpdateService>(
() => JadwalUpdateService(),
fenix: true, // Pastikan service tetap aktif selama aplikasi berjalan
);
}
}
}

View File

@ -298,7 +298,7 @@ class CalendarViewWidget extends StatelessWidget {
for (var jadwal in allJadwal) {
if (jadwal.tanggalPenyaluran != null) {
DateTime jadwalDate =
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
FormatHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
if (jadwalDate
.isAfter(firstDayOfMonth.subtract(const Duration(days: 1))) &&
@ -346,7 +346,7 @@ class CalendarViewWidget extends StatelessWidget {
void _showAppointmentDetails(BuildContext context, Appointment appointment) {
final String formattedDate =
DateTimeHelper.formatDateIndonesian(appointment.startTime);
FormatHelper.formatDateIndonesian(appointment.startTime);
// Dapatkan status dari ID jadwal
String? status = _getStatusFromAppointmentId(appointment.id);

View File

@ -207,7 +207,7 @@ class JadwalSectionWidget extends StatelessWidget {
// Format tanggal dan waktu menggunakan helper
String formattedDateTime =
DateTimeHelper.formatDateTime(jadwal.tanggalPenyaluran);
FormatHelper.formatDateTime(jadwal.tanggalPenyaluran);
// Dapatkan nama lokasi dan kategori
String lokasiName =

View File

@ -211,18 +211,16 @@ class DetailPenyaluranController extends GetxController {
.eq('id', penerima.id!)
.single();
if (penerimaData != null) {
final String stokBantuanId = penerimaData['stok_bantuan_id'];
final double jumlah = penerimaData['jumlah_bantuan'] is int
? penerimaData['jumlah_bantuan'].toDouble()
: penerimaData['jumlah_bantuan'];
final String stokBantuanId = penerimaData['stok_bantuan_id'];
final double jumlah = penerimaData['jumlah_bantuan'] is int
? penerimaData['jumlah_bantuan'].toDouble()
: penerimaData['jumlah_bantuan'];
// Kurangi stok dan catat riwayat
final petugasId = _supabaseService.client.auth.currentUser?.id;
if (petugasId != null) {
await _supabaseService.kurangiStokDariPenyaluran(
penerima.id!, stokBantuanId, jumlah, petugasId);
}
// Kurangi stok dan catat riwayat
final petugasId = _supabaseService.client.auth.currentUser?.id;
if (petugasId != null) {
await _supabaseService.kurangiStokDariPenyaluran(
penerima.id!, stokBantuanId, jumlah, petugasId);
}
// Refresh data setelah konfirmasi berhasil

View File

@ -11,14 +11,21 @@ import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'dart:async';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
import 'package:penyaluran_app/app/services/notification_service.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
class JadwalPenyaluranController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
late final JadwalUpdateService _jadwalUpdateService;
late final StreamSubscription _jadwalUpdateSubscription;
SupabaseService get supabaseService => _supabaseService;
final RxBool isLoading = false.obs;
final RxBool isLoadingStatusUpdate = false.obs;
final RxBool isLokasiLoading = false.obs;
// Indeks kategori yang dipilih untuk filter
final RxInt selectedCategoryIndex = 0.obs;
@ -52,6 +59,21 @@ class JadwalPenyaluranController extends GetxController {
@override
void onInit() {
super.onInit();
// Inisialisasi JadwalUpdateService
if (Get.isRegistered<JadwalUpdateService>()) {
_jadwalUpdateService = Get.find<JadwalUpdateService>();
} else {
_jadwalUpdateService = Get.put(JadwalUpdateService());
}
// Daftarkan controller ini untuk menerima pembaruan
_jadwalUpdateService.registerForUpdates('JadwalPenyaluranController');
// Berlangganan ke pembaruan jadwal
_jadwalUpdateSubscription =
_jadwalUpdateService.jadwalUpdateStream.listen(_handleJadwalUpdate);
loadJadwalData();
loadPermintaanPenjadwalanData();
loadLokasiPenyaluranData();
@ -67,100 +89,444 @@ class JadwalPenyaluranController extends GetxController {
searchController.dispose();
// Hentikan timer jika ada
_stopJadwalCheckTimer();
// Berhenti berlangganan pembaruan jadwal
_jadwalUpdateSubscription.cancel();
// Batalkan pendaftaran controller
_jadwalUpdateService.unregisterFromUpdates('JadwalPenyaluranController');
super.onClose();
}
// Timer untuk memeriksa jadwal secara berkala
Timer? _jadwalCheckTimer;
Timer?
_intensiveCheckTimer; // Timer untuk pengecekan intensif mendekati waktu penyaluran
final RxBool _intensiveCheckActive = false.obs; // Status pengecekan intensif
void _startJadwalCheckTimer() {
// Periksa jadwal setiap 1 menit
_jadwalCheckTimer = Timer.periodic(const Duration(minutes: 1), (_) {
checkAndUpdateJadwalStatus();
// Dengan fitur realtime yang sudah aktif, kita bisa mengurangi frekuensi polling
// Cek setiap 30 detik sebagai fallback untuk realtime
_jadwalCheckTimer = Timer.periodic(const Duration(seconds: 30), (_) {
if (!isLoadingStatusUpdate.value) {
checkAndUpdateJadwalStatus();
}
});
// Periksa jadwal segera saat aplikasi dimulai
checkAndUpdateJadwalStatus();
// Log info untuk debugging
print('Jadwal check timer started with 30 seconds interval');
// Mulai juga pengecekan jadwal yang akan datang
_startUpcomingJadwalCheck();
}
void _stopJadwalCheckTimer() {
_jadwalCheckTimer?.cancel();
_jadwalCheckTimer = null;
_intensiveCheckTimer?.cancel();
_intensiveCheckTimer = null;
}
// Metode baru untuk memeriksa jadwal mendatang dan memulai pemeriksaan intensif jika perlu
void _startUpcomingJadwalCheck() {
Timer.periodic(const Duration(minutes: 1), (timer) {
// Jika sudah ada timer intensif yang berjalan, tidak perlu melakukan pengecekan lagi
if (_intensiveCheckActive.value) return;
final now = DateTime.now();
bool foundUpcomingJadwal = false;
// Periksa apakah ada jadwal yang akan aktif dalam 10 menit ke depan
for (var jadwal in jadwalMendatang) {
if (jadwal.tanggalPenyaluran != null &&
jadwal.status == 'DIJADWALKAN') {
final jadwalTime = jadwal.tanggalPenyaluran!;
final diff = jadwalTime.difference(now).inMinutes;
// Jika ada jadwal dalam 10 menit ke depan, mulai pemeriksaan intensif
if (diff >= 0 && diff <= 10) {
print(
'Found upcoming jadwal in $diff minutes: ${jadwal.id} - ${jadwal.nama}');
foundUpcomingJadwal = true;
break;
}
}
}
// Jika ditemukan jadwal yang akan datang, mulai pemeriksaan intensif
if (foundUpcomingJadwal && !_intensiveCheckActive.value) {
_startIntensiveCheck();
}
});
}
// Metode untuk memulai pemeriksaan intensif untuk jadwal yang mendekati waktu
void _startIntensiveCheck() {
if (_intensiveCheckActive.value) return;
_intensiveCheckActive.value = true;
print('Starting intensive jadwal check every 5 seconds');
// Periksa setiap 5 detik
_intensiveCheckTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
if (!isLoadingStatusUpdate.value) {
checkAndUpdateJadwalStatus();
}
// Periksa apakah masih perlu melakukan pemeriksaan intensif
final now = DateTime.now();
bool needIntensiveCheck = false;
for (var jadwal in jadwalMendatang) {
if (jadwal.tanggalPenyaluran != null &&
jadwal.status == 'DIJADWALKAN') {
final jadwalTime = jadwal.tanggalPenyaluran!;
final diff = jadwalTime.difference(now).inMinutes;
// Jika masih ada jadwal dalam 10 menit ke depan, lanjutkan pemeriksaan
if (diff >= -5 && diff <= 10) {
needIntensiveCheck = true;
break;
}
}
}
// Jika tidak ada lagi jadwal yang mendekati waktu, hentikan pemeriksaan intensif
if (!needIntensiveCheck) {
_stopIntensiveCheck();
}
});
}
// Metode untuk menghentikan pemeriksaan intensif
void _stopIntensiveCheck() {
_intensiveCheckTimer?.cancel();
_intensiveCheckTimer = null;
_intensiveCheckActive.value = false;
print('Stopping intensive jadwal check');
}
// Handler untuk menerima pembaruan jadwal dari service
void _handleJadwalUpdate(Map<String, dynamic> updateData) {
if (updateData['type'] == 'status_update') {
// Update lokal jika jadwal yang diperbarui ada di salah satu list
final jadwalId = updateData['jadwal_id'];
final newStatus = updateData['new_status'];
// Periksa dan update jadwal di berbagai daftar
_updateJadwalStatusLocally(jadwalId, newStatus);
} else if (updateData['type'] == 'reload_required') {
// Muat ulang data jika diminta
loadJadwalData();
loadPermintaanPenjadwalanData();
} else if (updateData['type'] == 'check_required') {
// Segera periksa status jadwal
if (!isLoadingStatusUpdate.value) {
print(
'Received check_required signal, checking jadwal status immediately');
checkAndUpdateJadwalStatus();
} else {
print('Already checking jadwal status, ignoring check_required signal');
}
}
}
// Perbarui status jadwal secara lokal tanpa perlu memanggil API lagi
void _updateJadwalStatusLocally(String jadwalId, String newStatus) {
bool updated = false;
print(
'Updating jadwal status locally - ID: $jadwalId, New Status: $newStatus');
// Periksa jadwal aktif
final jadwalAktifIndex =
jadwalAktif.indexWhere((jadwal) => jadwal.id == jadwalId);
if (jadwalAktifIndex >= 0) {
print('Found in jadwalAktif at index $jadwalAktifIndex');
jadwalAktif[jadwalAktifIndex] =
jadwalAktif[jadwalAktifIndex].copyWith(status: newStatus);
updated = true;
}
// Periksa jadwal mendatang
final jadwalMendatangIndex =
jadwalMendatang.indexWhere((jadwal) => jadwal.id == jadwalId);
if (jadwalMendatangIndex >= 0) {
print('Found in jadwalMendatang at index $jadwalMendatangIndex');
jadwalMendatang[jadwalMendatangIndex] =
jadwalMendatang[jadwalMendatangIndex].copyWith(status: newStatus);
updated = true;
}
// Periksa jadwal terlaksana
final jadwalTerlaksanaIndex =
jadwalTerlaksana.indexWhere((jadwal) => jadwal.id == jadwalId);
if (jadwalTerlaksanaIndex >= 0) {
print('Found in jadwalTerlaksana at index $jadwalTerlaksanaIndex');
jadwalTerlaksana[jadwalTerlaksanaIndex] =
jadwalTerlaksana[jadwalTerlaksanaIndex].copyWith(status: newStatus);
updated = true;
}
// Jika perlu, reorganisasi daftar berdasarkan status baru
if (updated) {
print('Status updated locally, reorganizing lists');
_reorganizeJadwalLists();
// Perbarui counter penyaluran setelah reorganisasi daftar
_updatePenyaluranCounters();
} else {
print(
'Jadwal with ID $jadwalId not found in any list, refreshing data from server');
// Jika jadwal tidak ditemukan di daftar lokal, muat ulang data
loadJadwalData();
}
}
// Reorganisasi daftar jadwal berdasarkan status mereka
void _reorganizeJadwalLists() {
// Filter jadwal yang seharusnya pindah dari satu list ke list lain
// Jadwal yang seharusnya pindah dari aktif ke terlaksana
final completedJadwal = jadwalAktif
.where((j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA')
.toList();
if (completedJadwal.isNotEmpty) {
jadwalAktif.removeWhere(
(j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA');
jadwalTerlaksana.addAll(completedJadwal);
}
// Jadwal yang seharusnya pindah dari mendatang ke aktif
final activeJadwal =
jadwalMendatang.where((j) => j.status == 'AKTIF').toList();
if (activeJadwal.isNotEmpty) {
jadwalMendatang.removeWhere((j) => j.status == 'AKTIF');
jadwalAktif.addAll(activeJadwal);
}
// Jadwal yang seharusnya pindah dari mendatang ke terlaksana
final expiredJadwal = jadwalMendatang
.where((j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA')
.toList();
if (expiredJadwal.isNotEmpty) {
jadwalMendatang.removeWhere(
(j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA');
jadwalTerlaksana.addAll(expiredJadwal);
}
// Memicu pembaruan UI
jadwalAktif.refresh();
jadwalMendatang.refresh();
jadwalTerlaksana.refresh();
}
// Metode baru untuk memperbarui counter penyaluran
void _updatePenyaluranCounters() {
try {
// Dapatkan jumlah jadwal untuk setiap status
int dijadwalkan =
jadwalMendatang.where((j) => j.status == 'DIJADWALKAN').length;
int aktif = jadwalAktif.where((j) => j.status == 'AKTIF').length;
int batal =
jadwalTerlaksana.where((j) => j.status == 'BATALTERLAKSANA').length;
int terlaksana =
jadwalTerlaksana.where((j) => j.status == 'TERLAKSANA').length;
// Hitung total jadwal aktif untuk tab hari ini
int jadwalHariIni = jadwalAktif.length;
// Perbarui counter jadwal
if (Get.isRegistered<CounterService>()) {
final counterService = Get.find<CounterService>();
counterService.updateJadwalCounter(jadwalHariIni);
}
print(
'Jadwal counters updated - Aktif: $aktif, Dijadwalkan: $dijadwalkan, Terlaksana: $terlaksana, Batal: $batal');
} catch (e) {
print('Error updating jadwal counters: $e');
}
}
// Memeriksa dan memperbarui status jadwal
Future<void> checkAndUpdateJadwalStatus() async {
if (isLoadingStatusUpdate.value) return;
isLoadingStatusUpdate.value = true;
print('Starting jadwal status check at ${DateTime.now()}');
try {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
List<PenyaluranBantuanModel> jadwalToUpdate = [];
List<PenyaluranBantuanModel> jadwalTerlewat = [];
// Kelompokkan jadwal yang perlu diperbarui untuk mengurangi jumlah operasi database
final Map<String, String> jadwalUpdates = {};
final List<PenyaluranBantuanModel> jadwalToUpdate = [];
final List<PenyaluranBantuanModel> jadwalTerlewat = [];
for (var jadwal in jadwalAktif) {
if (jadwal.tanggalPenyaluran != null) {
final jadwalDateTime =
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
final jadwalDate = DateTime(
jadwalDateTime.year,
jadwalDateTime.month,
jadwalDateTime.day,
);
print('Checking ${jadwalMendatang.length} upcoming schedules');
if (isSameDay(jadwalDate, today)) {
if (now.isAfter(jadwalDateTime) ||
now.isAtSameMomentAs(jadwalDateTime)) {
if (jadwal.status == 'DIJADWALKAN') {
if (now
.isBefore(jadwalDateTime.add(const Duration(hours: 2)))) {
await _supabaseService.updateJadwalStatus(
jadwal.id!, 'AKTIF');
jadwalToUpdate.add(jadwal);
} else {
await _supabaseService.updateJadwalStatus(
jadwal.id!, 'BATALTERLAKSANA');
jadwalTerlewat.add(jadwal);
}
} else if (jadwal.status == 'AKTIF') {
if (now.isAfter(jadwalDateTime.add(const Duration(hours: 2)))) {
await _supabaseService.updateJadwalStatus(
jadwal.id!, 'BATALTERLAKSANA');
jadwalTerlewat.add(jadwal);
}
// Proses semua jadwal yang perlu diperbarui
for (var jadwal in jadwalMendatang) {
if (jadwal.tanggalPenyaluran != null && jadwal.id != null) {
final jadwalDate = jadwal.tanggalPenyaluran!;
// Log untuk debugging waktu pemeriksaan
print(
'Checking jadwal: ${jadwal.id} - ${jadwal.nama} scheduled for ${jadwal.tanggalPenyaluran}');
print('Current time: $now, Jadwal time: $jadwalDate');
// Periksa apakah jadwal sudah melewati waktunya
// Kita gunakan isAtSameMomentAs atau isAfter untuk menangkap dengan tepat
if (now.isAfter(jadwalDate) || now.isAtSameMomentAs(jadwalDate)) {
print('Jadwal time has passed/reached for ${jadwal.id}');
// Batasan 2 jam untuk status aktif
final batasAktif = jadwalDate.add(const Duration(hours: 2));
if (jadwal.status == 'DIJADWALKAN' && now.isBefore(batasAktif)) {
print(
'Updating to AKTIF: ${jadwal.id} - Time difference: ${now.difference(jadwalDate).inSeconds} seconds');
jadwalUpdates[jadwal.id!] = 'AKTIF';
jadwalToUpdate.add(jadwal);
} else if ((jadwal.status == 'DIJADWALKAN' ||
jadwal.status == 'AKTIF') &&
now.isAfter(batasAktif)) {
print('Updating to BATALTERLAKSANA (time expired): ${jadwal.id}');
jadwalUpdates[jadwal.id!] = 'BATALTERLAKSANA';
jadwalTerlewat.add(jadwal);
}
} else {
// Periksa apakah jadwal hampir memasuki waktunya (dalam 5 menit ke depan)
final diff = jadwalDate.difference(now).inMinutes;
if (diff >= 0 && diff <= 5 && jadwal.status == 'DIJADWALKAN') {
print('Jadwal will be active in $diff minutes: ${jadwal.id}');
// Tambahkan jadwal ke daftar pengawasan intensif
_jadwalUpdateService.addJadwalToWatch(jadwal.id!, jadwalDate);
// Jika tinggal 1 menit atau kurang, cek setiap 15 detik
if (diff <= 1) {
Future.delayed(const Duration(seconds: 15), () {
if (!isLoadingStatusUpdate.value) {
checkAndUpdateJadwalStatus();
}
});
}
}
}
}
}
if (jadwalToUpdate.isNotEmpty || jadwalTerlewat.isNotEmpty) {
await loadJadwalData();
// Update database hanya jika ada perubahan
if (jadwalUpdates.isNotEmpty) {
print('Batch updating ${jadwalUpdates.length} schedules');
if (jadwalToUpdate.isNotEmpty) {
Get.snackbar(
'Jadwal Diperbarui',
'${jadwalToUpdate.length} jadwal dipindahkan ke section Hari Ini',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
try {
// Gunakan batch update untuk meningkatkan efisiensi
await _supabaseService.batchUpdateJadwalStatus(jadwalUpdates);
if (jadwalTerlewat.isNotEmpty) {
Get.snackbar(
'Jadwal Terlewat',
'${jadwalTerlewat.length} jadwal diubah menjadi BATALTERLAKSANA',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.orange,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
// Perbarui data lokal
await loadJadwalData();
// Beritahu seluruh aplikasi tentang pembaruan
await _jadwalUpdateService.notifyJadwalUpdate();
// Kirim notifikasi untuk perubahan status jadwal
bool notificationsSuccessful = true;
final notificationService = Get.find<NotificationService>();
try {
// Kirim notifikasi untuk jadwal yang diperbarui menjadi Aktif
for (var jadwal in jadwalToUpdate) {
if (jadwal.id != null && jadwal.nama != null) {
await notificationService.sendJadwalStatusNotification(
jadwalId: jadwal.id!,
newStatus: 'AKTIF',
jadwalNama: jadwal.nama!,
);
}
}
} catch (notificationError) {
print(
'Warning: Error sending AKTIF notifications: $notificationError');
notificationsSuccessful = false;
}
try {
// Kirim notifikasi untuk jadwal yang terlewat
for (var jadwal in jadwalTerlewat) {
if (jadwal.id != null && jadwal.nama != null) {
await notificationService.sendJadwalStatusNotification(
jadwalId: jadwal.id!,
newStatus: 'BATALTERLAKSANA',
jadwalNama: jadwal.nama!,
);
}
}
} catch (notificationError) {
print(
'Warning: Error sending BATALTERLAKSANA notifications: $notificationError');
notificationsSuccessful = false;
}
// Tampilkan notifikasi hanya jika ada perubahan
if (jadwalToUpdate.isNotEmpty) {
Get.snackbar(
'Jadwal Diperbarui',
'${jadwalToUpdate.length} jadwal dipindahkan ke section Hari Ini',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
if (jadwalTerlewat.isNotEmpty) {
Get.snackbar(
'Jadwal Terlewat',
'${jadwalTerlewat.length} jadwal diubah menjadi BATALTERLAKSANA',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.orange,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
// Log status keseluruhan
if (notificationsSuccessful) {
print(
'Jadwal status update and notifications completed successfully');
} else {
print('Jadwal status update completed with notification errors');
}
} catch (updateError) {
print('Error during batch update process: $updateError');
// Jika batch update gagal, coba update satu-per-satu secara manual
print('Trying individual updates for critical jadwal...');
// Prioritaskan jadwal yang akan diaktifkan
for (var jadwal in jadwalToUpdate) {
if (jadwal.id != null) {
try {
await _supabaseService.updateJadwalStatus(jadwal.id!, 'AKTIF');
print('Manual update successful for jadwal ${jadwal.id}');
} catch (e) {
print('Manual update failed for jadwal ${jadwal.id}: $e');
}
}
}
}
} else {
print('No schedule updates needed');
}
} catch (e, stackTrace) {
print('Error checking and updating jadwal status: $e');
print('Stack trace: $stackTrace');
} finally {
isLoadingStatusUpdate.value = false;
print('Jadwal status check completed at ${DateTime.now()}');
}
}
@ -197,6 +563,9 @@ class JadwalPenyaluranController extends GetxController {
.map((data) => PenyaluranBantuanModel.fromJson(data))
.toList();
}
// Perbarui counter penyaluran setelah data dimuat
_updatePenyaluranCounters();
} catch (e) {
print('Error loading jadwal data: $e');
} finally {
@ -220,6 +589,7 @@ class JadwalPenyaluranController extends GetxController {
Future<void> loadLokasiPenyaluranData() async {
try {
isLokasiLoading(true);
final lokasiData = await _supabaseService.getAllLokasiPenyaluran();
if (lokasiData != null) {
for (var lokasi in lokasiData) {
@ -229,6 +599,8 @@ class JadwalPenyaluranController extends GetxController {
}
} catch (e) {
print('Error loading lokasi penyaluran data: $e');
} finally {
isLokasiLoading(false);
}
}
@ -335,8 +707,30 @@ class JadwalPenyaluranController extends GetxController {
Future<void> completeJadwal(String jadwalId) async {
isLoading.value = true;
try {
// Dapatkan detail jadwal
final jadwalIndex = jadwalAktif.indexWhere((j) => j.id == jadwalId);
PenyaluranBantuanModel? jadwal;
if (jadwalIndex >= 0) {
jadwal = jadwalAktif[jadwalIndex];
}
// Update status di database
await _supabaseService.completeJadwal(jadwalId);
// Kirim notifikasi
if (jadwal != null && jadwal.nama != null) {
final notificationService = Get.find<NotificationService>();
await notificationService.sendJadwalStatusNotification(
jadwalId: jadwalId,
newStatus: 'TERLAKSANA',
jadwalNama: jadwal.nama!,
);
}
// Reload data
await loadJadwalData();
Get.snackbar(
'Sukses',
'Jadwal berhasil diselesaikan',
@ -359,15 +753,13 @@ class JadwalPenyaluranController extends GetxController {
}
Future<void> refreshData() async {
isLoading.value = true;
try {
await loadJadwalData();
await loadPermintaanPenjadwalanData();
} catch (e) {
print('Error refreshing data: $e');
} finally {
isLoading.value = false;
}
await Future.wait([
loadJadwalData(),
loadPermintaanPenjadwalanData(),
loadLokasiPenyaluranData(),
loadKategoriBantuanData(),
loadSkemaBantuanData(),
]);
}
void changeCategory(int index) {
@ -431,6 +823,7 @@ class JadwalPenyaluranController extends GetxController {
'status_penerimaan': 'BELUMMENERIMA',
'qr_code_hash': qrCodeHash,
'jumlah_bantuan': jumlahDiterimaPerOrang,
'created_at': DateTime.now().toIso8601String(),
};
// Simpan data penerima ke database

View File

@ -96,10 +96,10 @@ class PelaksanaanPenyaluranController extends GetxController {
? response['kategori_bantuan']['nama']
: 'Tidak tersedia',
'tanggal': penyaluranModel.tanggalPenyaluran != null
? DateTimeHelper.formatDate(penyaluranModel.tanggalPenyaluran!)
? FormatHelper.formatDateTime(penyaluranModel.tanggalPenyaluran!)
: 'Tidak tersedia',
'waktu': penyaluranModel.tanggalPenyaluran != null
? DateTimeHelper.formatTime(penyaluranModel.tanggalPenyaluran!)
? FormatHelper.formatTime(penyaluranModel.tanggalPenyaluran!)
: 'Tidak tersedia',
'jumlah_penerima': penyaluranModel.jumlahPenerima?.toString() ?? '0',
'status': penyaluranModel.status,

View File

@ -289,7 +289,7 @@ class PenerimaController extends GetxController {
);
if (picked != null) {
tanggalPenyaluran.value = DateTimeHelper.formatDate(picked);
tanggalPenyaluran.value = FormatHelper.formatDateTime(picked);
}
}

View File

@ -8,6 +8,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_serv
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart';
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
class PetugasDesaController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
@ -182,10 +183,22 @@ class PetugasDesaController extends GetxController {
}
_counterService = Get.find<CounterService>();
// Pastikan JadwalUpdateService juga tersedia
JadwalUpdateService jadwalUpdateService;
if (Get.isRegistered<JadwalUpdateService>()) {
jadwalUpdateService = Get.find<JadwalUpdateService>();
} else {
jadwalUpdateService = Get.put(JadwalUpdateService());
}
// Perbarui counter pada saat aplikasi dimulai
jadwalUpdateService.refreshCounters();
// Muat data awal
loadUserProfile();
loadNotifikasiData();
loadJadwalData();
loadPenitipanData();
loadJadwalData();
loadNotifikasiData();
loadPengaduanData();
}

View File

@ -5,11 +5,15 @@ import 'package:penyaluran_app/app/data/models/notifikasi_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
import 'dart:async';
class PetugasDesaDashboardController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
late final CounterService _counterService;
late final JadwalUpdateService _jadwalUpdateService;
late StreamSubscription _jadwalUpdateSubscription;
final RxBool isLoading = false.obs;
@ -67,18 +71,47 @@ class PetugasDesaDashboardController extends GetxController {
}
_counterService = Get.find<CounterService>();
// Inisialisasi JadwalUpdateService untuk pembaruan realtime
if (Get.isRegistered<JadwalUpdateService>()) {
_jadwalUpdateService = Get.find<JadwalUpdateService>();
} else {
_jadwalUpdateService = Get.put(JadwalUpdateService());
}
// Daftarkan controller ini untuk menerima pembaruan
_jadwalUpdateService.registerForUpdates('PetugasDesaDashboardController');
// Berlangganan ke pembaruan jadwal
_jadwalUpdateSubscription =
_jadwalUpdateService.jadwalUpdateStream.listen(_handleJadwalUpdate);
loadUserProfile();
loadDashboardData();
loadNotifikasiData();
loadJadwalAktif();
loadJadwalHariIni();
}
@override
void onClose() {
// Berhenti berlangganan pembaruan jadwal
_jadwalUpdateSubscription.cancel();
// Batalkan pendaftaran controller
_jadwalUpdateService
.unregisterFromUpdates('PetugasDesaDashboardController');
searchController.dispose();
super.onClose();
}
// Handler untuk menerima pembaruan jadwal dari service
void _handleJadwalUpdate(Map<String, dynamic> updateData) {
if (updateData['type'] == 'status_update' ||
updateData['type'] == 'reload_required' ||
updateData['type'] == 'check_required') {
// Muat ulang data dashboard saat ada perubahan status jadwal
loadDashboardData();
}
}
// Metode untuk memuat data profil pengguna dari cache
Future<void> loadUserProfile() async {
try {
@ -155,14 +188,14 @@ class PetugasDesaDashboardController extends GetxController {
}
}
Future<void> loadJadwalAktif() async {
Future<void> loadJadwalHariIni() async {
try {
final jadwalData = await _supabaseService.getJadwalAktif();
if (jadwalData != null) {
jadwalHariIni.value = jadwalData;
}
} catch (e) {
print('Error loading jadwal hari ini: $e');
print('Error loading jadwal data: $e');
}
}
@ -173,7 +206,7 @@ class PetugasDesaDashboardController extends GetxController {
loadUserProfile(),
loadDashboardData(),
loadNotifikasiData(),
loadJadwalAktif(),
loadJadwalHariIni(),
]);
} catch (e) {
print('Error refreshing data: $e');

View File

@ -221,15 +221,25 @@ class DaftarPenerimaView extends GetView<PenerimaController> {
),
child: CircleAvatar(
radius: 35,
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
backgroundImage: penerima['foto_profil'] != null
backgroundColor: AppTheme.primaryColor.withOpacity(0.2),
backgroundImage: penerima['foto_profil'] != null &&
penerima['foto_profil'].toString().isNotEmpty
? NetworkImage(penerima['foto_profil'])
: null,
child: penerima['foto_profil'] == null
? Icon(
Icons.person,
size: 35,
color: AppTheme.primaryColor.withOpacity(0.7),
child: (penerima['foto_profil'] == null ||
penerima['foto_profil'].toString().isEmpty)
? Text(
penerima['nama_lengkap'] != null
? penerima['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 24,
),
)
: null,
),
@ -435,13 +445,24 @@ class PenerimaSearchDelegate extends SearchDelegate {
},
leading: CircleAvatar(
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
backgroundImage: penerima['foto_profil'] != null
backgroundImage: penerima['foto_profil'] != null &&
penerima['foto_profil'].toString().isNotEmpty
? NetworkImage(penerima['foto_profil'])
: null,
child: penerima['foto_profil'] == null
? const Icon(
Icons.person,
color: AppTheme.primaryColor,
child: (penerima['foto_profil'] == null ||
penerima['foto_profil'].toString().isEmpty)
? Text(
penerima['nama_lengkap'] != null
? penerima['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 24,
),
)
: null,
),

View File

@ -33,6 +33,58 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header DisalurKita dengan logo dan slogan
FadeInAnimation(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Image.asset(
'assets/images/logo-disalurkita.png',
width: 50,
height: 50,
),
const SizedBox(width: 15),
const Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'DisalurKita',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF1565C0),
),
),
SizedBox(height: 5),
Text(
'Salurkan dengan Pasti, Pantau dengan Bukti',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
),
const SizedBox(height: 20),
// Header dengan greeting
FadeInAnimation(
child: GreetingHeader(
@ -83,7 +135,7 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jadwal Penyaluran',
'Jadwal Penyaluran Hari Ini',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -130,19 +182,25 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
final DateTime tanggal =
DateTime.parse(jadwal['tanggal_penyaluran']);
final String formattedDate =
DateTimeHelper.formatDateTime(tanggal);
FormatHelper.formatDateTime(tanggal);
final kategoriBantuan =
jadwal['kategori_bantuan'] as Map<String, dynamic>;
final lokasiPenyaluran =
jadwal['lokasi_penyaluran'] as Map<String, dynamic>;
return ScheduleCard(
title: kategoriBantuan['nama'] ?? 'Jadwal Penyaluran',
location: lokasiPenyaluran['nama'] ?? 'Lokasi tidak tersedia',
dateTime: formattedDate,
isToday: true,
onTap: () => Get.toNamed(Routes.detailPenyaluran,
parameters: {'id': jadwal['id']}),
return Column(
children: [
if (index > 0) const SizedBox(height: 10),
ScheduleCard(
title: kategoriBantuan['nama'] ?? 'Jadwal Penyaluran',
location:
lokasiPenyaluran['nama'] ?? 'Lokasi tidak tersedia',
dateTime: formattedDate,
isToday: true,
onTap: () => Get.toNamed(Routes.detailPenyaluran,
parameters: {'id': jadwal['id']}),
),
],
);
},
);
@ -391,8 +449,10 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
final nik = penerima['nik'] ?? 'NIK tidak tersedia';
final status = penerima['status'] ?? 'AKTIF';
final id = penerima['id'] ?? 'ID tidak tersedia';
final fotoProfil = penerima['foto_profil'] ?? null;
return _buildRecipientItem(name, nik, status, id, textTheme);
return _buildRecipientItem(
name, nik, status, id, textTheme, fotoProfil);
},
);
},
@ -401,8 +461,8 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
);
}
Widget _buildRecipientItem(
String name, String nik, String status, String id, TextTheme textTheme) {
Widget _buildRecipientItem(String name, String nik, String status, String id,
TextTheme textTheme, String? fotoProfil) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 10),
@ -428,7 +488,20 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
children: [
CircleAvatar(
backgroundColor: Colors.white.withOpacity(0.2),
child: const Icon(Icons.person, color: Colors.white),
backgroundImage:
fotoProfil != null && fotoProfil.toString().isNotEmpty
? NetworkImage(fotoProfil)
: null,
child: (fotoProfil == null || fotoProfil.toString().isEmpty)
? Text(
name.toString().substring(0, 1).toUpperCase(),
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 24,
),
)
: null,
),
const SizedBox(width: 12),
Expanded(

View File

@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/data/models/donatur_model.dart';
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class DetailDonaturView extends GetView<DonaturController> {
const DetailDonaturView({super.key});
@ -359,7 +360,7 @@ class DetailDonaturView extends GetView<DonaturController> {
Icons.calendar_today,
'Terdaftar Sejak',
donatur.createdAt != null
? DateTimeHelper.formatDate(donatur.createdAt!)
? FormatHelper.formatDateTime(donatur.createdAt!)
: 'Tidak diketahui',
),
],
@ -514,7 +515,8 @@ class DetailDonaturView extends GetView<DonaturController> {
Widget _buildDonasiItem(PenitipanBantuanModel penitipan) {
final isUang = penitipan.isUang == true;
final tanggal = penitipan.createdAt != null
? DateTimeHelper.formatDate(penitipan.createdAt!, format: 'dd MMM yyyy')
? FormatHelper.formatDateTime(penitipan.createdAt!,
format: 'dd MMM yyyy')
: 'Tanggal tidak diketahui';
String nilaiDonasi = '';
@ -626,7 +628,7 @@ class DetailDonaturView extends GetView<DonaturController> {
getPetugasDesaNama: (String? id) =>
controller.getPetugasDesaNama(id) ?? 'Petugas tidak diketahui',
showFullScreenImage: (String imageUrl) {
DetailPenitipanDialog.showFullScreenImage(Get.context!, imageUrl);
ShowImageDialog.showFullScreen(Get.context!, imageUrl);
},
);
}

View File

@ -107,14 +107,24 @@ class DetailPenerimaView extends GetView<PenerimaController> {
child: CircleAvatar(
radius: 60,
backgroundColor: Colors.white,
backgroundImage: penerima['foto_profil'] != null
backgroundImage: penerima['foto_profil'] != null &&
penerima['foto_profil'].toString().isNotEmpty
? NetworkImage(penerima['foto_profil'])
: null,
child: penerima['foto_profil'] == null
? Icon(
Icons.person,
size: 60,
color: AppTheme.primaryColor.withOpacity(0.7),
child: (penerima['foto_profil'] == null ||
penerima['foto_profil'].toString().isEmpty)
? Text(
penerima['nama_lengkap'] != null
? penerima['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor.withOpacity(0.7),
fontSize: 36,
),
)
: null,
),
@ -507,7 +517,7 @@ class DetailPenerimaView extends GetView<PenerimaController> {
child: _buildInfoItem(
Icons.calendar_today,
'Tanggal Penerimaan',
DateTimeHelper.formatDateTime(tanggalPenerimaan),
FormatHelper.formatDateTime(tanggalPenerimaan),
),
),
Expanded(

View File

@ -1,13 +1,12 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/cards/info_card.dart';
import 'package:penyaluran_app/app/widgets/indicators/status_pill.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:timeline_tile/timeline_tile.dart';
import 'package:image_picker/image_picker.dart';
@ -15,7 +14,7 @@ import 'dart:io';
import 'package:penyaluran_app/app/widgets/inputs/dropdown_input.dart';
import 'package:penyaluran_app/app/widgets/inputs/text_input.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class DetailPengaduanView extends GetView<PengaduanController> {
const DetailPengaduanView({super.key});
@ -1092,8 +1091,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
child: Row(
children: tindakan.buktiTindakan!.map((bukti) {
return GestureDetector(
onTap: () =>
showFullScreenImage(context, bukti),
onTap: () => ShowImageDialog.showFullScreen(
context, bukti),
child: Container(
width: 100,
height: 100,
@ -1190,8 +1189,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
Expanded(
child: Text(
tindakan.tanggalTindakan != null
? DateFormat('dd MMM yyyy HH:mm', 'id_ID')
.format(tindakan.tanggalTindakan!)
? FormatHelper.formatDateTime(
tindakan.tanggalTindakan!)
: '-',
style: TextStyle(
fontSize: 12,
@ -1669,9 +1668,11 @@ class DetailPengaduanView extends GetView<PengaduanController> {
return Stack(
children: [
GestureDetector(
onTap: () => showFullScreenImage(
stateContext,
buktiTindakanPaths[index]),
onTap: () => ShowImageDialog
.showFullScreen(
stateContext,
buktiTindakanPaths[
index]),
child: Container(
width: 100,
height: 100,
@ -2003,63 +2004,6 @@ class DetailPengaduanView extends GetView<PengaduanController> {
);
}
void showFullScreenImage(BuildContext context, String imagePath) {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
insetPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
child: Stack(
alignment: Alignment.center,
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black87,
),
),
InteractiveViewer(
panEnabled: true,
boundaryMargin: const EdgeInsets.all(20),
minScale: 0.5,
maxScale: 4.0,
child: CachedNetworkImage(
imageUrl: imagePath,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error, color: Colors.white, size: 32),
const SizedBox(height: 8),
Text(
'Gagal memuat gambar',
style: TextStyle(color: Colors.white),
),
],
),
),
),
Positioned(
top: 20,
right: 20,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.white, size: 30),
onPressed: () => Navigator.pop(context),
),
),
],
),
);
},
);
}
// Widget untuk menampilkan feedback dan rating warga
Widget _buildFeedbackSection(BuildContext context, PengaduanModel pengaduan) {
return Card(
elevation: 3,
@ -2348,8 +2292,7 @@ class DetailPengaduanView extends GetView<PengaduanController> {
const SizedBox(width: 12),
Text(
pengaduan.tanggalPengaduan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(pengaduan.tanggalPengaduan!)
? FormatHelper.formatDateTime(pengaduan.tanggalPengaduan!)
: '-',
style: TextStyle(
fontSize: 15,
@ -2376,7 +2319,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: GestureDetector(
onTap: () => _showFullScreenImage(context, url),
onTap: () =>
ShowImageDialog.showFullScreen(context, url),
child: Container(
width: 120,
decoration: BoxDecoration(
@ -2589,57 +2533,4 @@ class DetailPengaduanView extends GetView<PengaduanController> {
);
}
}
void _showFullScreenImage(BuildContext context, String imagePath) {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
insetPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
child: Stack(
children: [
InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 4,
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.7),
child: Center(
child: imagePath.startsWith('http')
? CachedNetworkImage(
imageUrl: imagePath,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => const Icon(
Icons.error,
color: Colors.red,
size: 50,
),
)
: Image.file(File(imagePath)),
),
),
),
Positioned(
top: 20,
right: 20,
child: IconButton(
icon: const Icon(
Icons.close,
color: Colors.white,
size: 30,
),
onPressed: () => Navigator.pop(context),
),
),
],
),
);
},
);
}
}

View File

@ -267,7 +267,7 @@ class DetailPenyaluranPage extends StatelessWidget {
Icons.event,
'Tanggal Penyaluran',
penyaluran.tanggalPenyaluran != null
? DateTimeHelper.formatDateTime(
? FormatHelper.formatDateTime(
penyaluran.tanggalPenyaluran!)
: 'Belum dijadwalkan',
AppTheme.secondaryColor),
@ -280,7 +280,7 @@ class DetailPenyaluranPage extends StatelessWidget {
Icons.event_available,
'Tanggal Selesai',
penyaluran.tanggalSelesai != null
? DateTimeHelper.formatDateTime(
? FormatHelper.formatDateTime(
penyaluran.tanggalSelesai!)
: '-',
AppTheme.secondaryColor),
@ -1065,19 +1065,30 @@ class DetailPenyaluranPage extends StatelessWidget {
backgroundColor: sudahMenerima
? statusColor.withOpacity(0.15)
: Colors.grey.shade50,
child: Text(
warga != null && warga['nama_lengkap'] != null
? warga['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: sudahMenerima ? statusColor : Colors.grey.shade700,
fontSize: 22,
),
),
backgroundImage: warga != null &&
warga['foto_profil'] != null &&
warga['foto_profil'].toString().isNotEmpty
? NetworkImage(warga['foto_profil'])
: null,
child: (warga == null ||
warga['foto_profil'] == null ||
warga['foto_profil'].toString().isEmpty)
? Text(
warga != null && warga['nama_lengkap'] != null
? warga['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: sudahMenerima
? statusColor
: Colors.grey.shade700,
fontSize: 22,
),
)
: null,
),
),
const SizedBox(width: 16),
@ -1621,19 +1632,28 @@ class DetailPenyaluranPage extends StatelessWidget {
CircleAvatar(
radius: 30,
backgroundColor: statusColor.withOpacity(0.2),
child: Text(
warga != null && warga['nama_lengkap'] != null
? warga['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: statusColor,
fontSize: 24,
),
),
backgroundImage: warga != null &&
warga['foto_profil'] != null &&
warga['foto_profil'].toString().isNotEmpty
? NetworkImage(warga['foto_profil'])
: null,
child: (warga == null ||
warga['foto_profil'] == null ||
warga['foto_profil'].toString().isEmpty)
? Text(
warga != null && warga['nama_lengkap'] != null
? warga['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: statusColor,
fontSize: 24,
),
)
: null,
),
const SizedBox(width: 16),
Expanded(
@ -1753,7 +1773,7 @@ class DetailPenyaluranPage extends StatelessWidget {
if (penerima.tanggalPenerimaan != null)
_buildInfoRow(
'Tanggal Penerimaan',
DateTimeHelper.formatDate(
FormatHelper.formatDateTime(
penerima.tanggalPenerimaan!)),
if (penerima.jumlahBantuan != null)
_buildInfoRow('Jumlah Bantuan',
@ -1946,7 +1966,7 @@ class DetailPenyaluranPage extends StatelessWidget {
_buildInfoRow('Status', 'Batal Terlaksana'),
if (penyaluran.tanggalSelesai != null)
_buildInfoRow('Tanggal Pembatalan',
DateTimeHelper.formatDateTime(penyaluran.tanggalSelesai!)),
FormatHelper.formatDateTime(penyaluran.tanggalSelesai!)),
const SizedBox(height: 8),
const Text(
'Alasan Pembatalan:',
@ -2126,7 +2146,7 @@ class DetailPenyaluranPage extends StatelessWidget {
_buildInfoRow(
'Tanggal Laporan',
controller.laporan.value?.tanggalLaporan != null
? DateTimeHelper.formatDateTime(
? FormatHelper.formatDateTime(
controller.laporan.value!.tanggalLaporan!)
: '-',
),

View File

@ -198,7 +198,7 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
'Tempat, Tanggal Lahir',
warga?['tempat_lahir'] != null &&
warga?['tanggal_lahir'] != null
? '${warga!['tempat_lahir']}, ${DateTimeHelper.formatDate(DateTime.parse(warga['tanggal_lahir']), format: 'd MMMM yyyy')}'
? '${warga!['tempat_lahir']}, ${FormatHelper.formatDateTime(DateTime.parse(warga['tanggal_lahir']), format: 'd MMMM yyyy')}'
: 'Bogor, 2 Juni 1990'),
const Divider(),
@ -236,18 +236,18 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
String tanggalWaktuPenyaluran = '';
if (widget.tanggalPenyaluran != null) {
final tanggal = DateTimeHelper.formatDate(widget.tanggalPenyaluran!);
final waktuMulai = DateTimeHelper.formatTime(widget.tanggalPenyaluran!);
final waktuSelesai = DateTimeHelper.formatTime(
final tanggal = FormatHelper.formatDateTime(widget.tanggalPenyaluran!);
final waktuMulai = FormatHelper.formatTime(widget.tanggalPenyaluran!);
final waktuSelesai = FormatHelper.formatTime(
widget.tanggalPenyaluran!.add(const Duration(hours: 1)));
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
} else if (penerima.penyaluranBantuan != null &&
penerima.penyaluranBantuan!['tanggal_penyaluran'] != null) {
final tanggalPenyaluran =
DateTime.parse(penerima.penyaluranBantuan!['tanggal_penyaluran']);
final tanggal = DateTimeHelper.formatDate(tanggalPenyaluran);
final waktuMulai = DateTimeHelper.formatTime(tanggalPenyaluran);
final waktuSelesai = DateTimeHelper.formatTime(
final tanggal = FormatHelper.formatDateTime(tanggalPenyaluran);
final waktuMulai = FormatHelper.formatTime(tanggalPenyaluran);
final waktuSelesai = FormatHelper.formatTime(
tanggalPenyaluran.add(const Duration(hours: 1)));
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
} else {

View File

@ -44,7 +44,7 @@ class PengaduanView extends GetView<PengaduanController> {
Widget _buildLastUpdateInfo(BuildContext context) {
final lastUpdate = DateTime
.now(); // Gunakan waktu saat ini atau dari controller jika tersedia
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
return Padding(
padding: const EdgeInsets.only(top: 8.0),
@ -280,7 +280,7 @@ class PengaduanView extends GetView<PengaduanController> {
),
),
Text(
'${DateTimeHelper.formatNumber(filteredPengaduan.length)} item',
'${FormatHelper.formatNumber(filteredPengaduan.length)} item',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
@ -320,7 +320,7 @@ class PengaduanView extends GetView<PengaduanController> {
// Format tanggal menggunakan DateTimeHelper
String formattedDate = '';
if (item.tanggalPengaduan != null) {
formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan);
formattedDate = FormatHelper.formatDateTime(item.tanggalPengaduan);
}
return Card(

View File

@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_ba
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
import 'dart:io';
class PenitipanView extends GetView<PenitipanBantuanController> {
@ -72,7 +73,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
context,
icon: Icons.pending_actions,
title: 'Menunggu',
value: DateTimeHelper.formatNumber(
value: FormatHelper.formatNumber(
controller.jumlahMenunggu.value),
color: Colors.orange,
),
@ -82,7 +83,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
context,
icon: Icons.check_circle,
title: 'Terverifikasi',
value: DateTimeHelper.formatNumber(
value: FormatHelper.formatNumber(
controller.jumlahTerverifikasi.value),
color: Colors.green,
),
@ -92,8 +93,8 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
context,
icon: Icons.cancel,
title: 'Ditolak',
value: DateTimeHelper.formatNumber(
controller.jumlahDitolak.value),
value:
FormatHelper.formatNumber(controller.jumlahDitolak.value),
color: Colors.red,
),
),
@ -219,7 +220,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
),
),
Text(
'${DateTimeHelper.formatNumber(filteredList.length)} item',
'${FormatHelper.formatNumber(filteredList.length)} item',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
@ -360,7 +361,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
],
),
Text(
DateTimeHelper.formatDate(item.createdAt),
FormatHelper.formatDateTime(item.createdAt),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey.shade700,
fontStyle: FontStyle.italic,
@ -380,15 +381,27 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
Row(
children: [
CircleAvatar(
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
radius: 20,
child: Text(
donaturNama.substring(0, 1).toUpperCase(),
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
),
),
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
backgroundImage: item.donatur != null &&
item.donatur!.fotoProfil != null &&
item.donatur!.fotoProfil!.isNotEmpty
? NetworkImage(item.donatur!.fotoProfil!)
: null,
child: (item.donatur == null ||
item.donatur!.fotoProfil == null ||
item.donatur!.fotoProfil!.isEmpty)
? Text(
donaturNama.isNotEmpty
? donaturNama.substring(0, 1).toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 16,
),
)
: null,
),
const SizedBox(width: 12),
Expanded(
@ -546,8 +559,8 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
const SizedBox(height: 4),
Text(
isUang
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan',
? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
: '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan',
style: Theme.of(context)
.textTheme
.titleSmall
@ -947,7 +960,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
kategoriSatuan: kategoriSatuan,
getPetugasDesaNama: (String? id) => controller.getPetugasDesaNama(id),
showFullScreenImage: (String imageUrl) {
DetailPenitipanDialog.showFullScreenImage(context, imageUrl);
ShowImageDialog.showFullScreen(context, imageUrl);
},
);
}
@ -992,7 +1005,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
Widget _buildLastUpdateInfo(BuildContext context) {
return Obx(() {
final lastUpdate = controller.lastUpdateTime.value;
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
return Padding(
padding: const EdgeInsets.only(top: 8.0),

View File

@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/jadwal_section_widget.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/calendar_view_widget.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_view.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
class PenyaluranView extends GetView<JadwalPenyaluranController> {
const PenyaluranView({super.key});
@ -41,13 +42,20 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => Get.to(() => const TambahPenyaluranView()),
backgroundColor: AppTheme.primaryColor,
icon: const Icon(Icons.add, color: Colors.white),
label: const Text('Tambah Jadwal',
style: TextStyle(color: Colors.white)),
elevation: 2,
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Tombol untuk menambah jadwal penyaluran
FloatingActionButton.extended(
heroTag: 'tambahJadwal',
onPressed: () => Get.to(() => const TambahPenyaluranView()),
backgroundColor: AppTheme.primaryColor,
icon: const Icon(Icons.add, color: Colors.white),
label: const Text('Tambah Jadwal',
style: TextStyle(color: Colors.white)),
elevation: 2,
),
],
),
),
);
@ -76,6 +84,11 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
// Ringkasan jadwal
_buildJadwalSummary(Get.context!),
const SizedBox(height: 16),
// Tombol untuk mengelola lokasi penyaluran
_buildLokasiPenyaluranSection(),
const SizedBox(height: 24),
// Jadwal hari ini
@ -224,4 +237,240 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
],
);
}
// Widget untuk menampilkan section lokasi penyaluran
Widget _buildLokasiPenyaluranSection() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.blue.shade100, width: 1),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Lokasi Penyaluran',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
),
OutlinedButton.icon(
onPressed: () {
// Menampilkan dialog daftar lokasi penyaluran
_showLokasiPenyaluranDialog();
},
icon: const Icon(Icons.map, size: 16),
label: const Text('Lihat Lokasi'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.blue,
side: BorderSide(color: Colors.blue.shade300),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
),
),
],
),
const SizedBox(height: 8),
Text(
'Kelola lokasi penyaluran bantuan untuk masyarakat dengan lebih mudah',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: () => Get.toNamed(Routes.tambahLokasiPenyaluran),
icon: const Icon(Icons.add_location, size: 16),
label: const Text('Tambah Lokasi Penyaluran Baru'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade50,
foregroundColor: Colors.blue.shade700,
padding:
const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.blue.shade200),
),
),
),
],
),
),
);
}
// Fungsi untuk menampilkan dialog daftar lokasi penyaluran
void _showLokasiPenyaluranDialog() {
Get.dialog(
Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Daftar Lokasi Penyaluran',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
),
IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close),
visualDensity: VisualDensity.compact,
),
],
),
const SizedBox(height: 12),
Container(
constraints: BoxConstraints(
maxHeight: Get.height * 0.5,
),
width: double.infinity,
child: Obx(() {
if (controller.isLokasiLoading.value) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (controller.lokasiPenyaluranCache.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.location_off,
size: 48,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Belum ada lokasi penyaluran',
style: TextStyle(
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () {
Get.back();
Get.toNamed(Routes.tambahLokasiPenyaluran);
},
icon: const Icon(Icons.add_location),
label: const Text('Tambah Lokasi'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
),
],
),
);
}
return ListView.builder(
shrinkWrap: true,
itemCount: controller.lokasiPenyaluranCache.length,
itemBuilder: (context, index) {
final lokasi = controller.lokasiPenyaluranCache.values
.elementAt(index);
final lokasiId = controller.lokasiPenyaluranCache.keys
.elementAt(index);
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text(
lokasi.nama,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (lokasi.alamat != null &&
lokasi.alamat!.isNotEmpty)
Text(lokasi.alamat!),
Row(
children: [
if (lokasi.isLokasiTitip)
Container(
margin: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Lokasi Penitipan',
style: TextStyle(
fontSize: 10,
color: Colors.green.shade800,
),
),
),
],
),
],
),
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
shape: BoxShape.circle,
),
child: Icon(
Icons.location_on,
color: Colors.blue.shade700,
),
),
),
);
},
);
}),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: () {
Get.back();
Get.toNamed(Routes.tambahLokasiPenyaluran);
},
child: const Text('Tambah Lokasi Baru'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.blue,
),
),
],
),
],
),
),
),
);
}
}

View File

@ -39,7 +39,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
case 4:
return const Text('Stok Bantuan');
default:
return const Text('Petugas Desa');
return const Text('Dashboard');
}
}),
leading: IconButton(
@ -223,14 +223,23 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
child: CircleAvatar(
radius: 40,
backgroundColor: Colors.white70,
backgroundImage: controller.profilePhotoUrl != null
backgroundImage: controller.profilePhotoUrl != null &&
controller.profilePhotoUrl!.isNotEmpty
? NetworkImage(controller.profilePhotoUrl!)
: null,
child: controller.profilePhotoUrl == null
? Icon(
Icons.person,
color: Colors.white,
size: 40,
child: (controller.profilePhotoUrl == null ||
controller.profilePhotoUrl!.isEmpty)
? Text(
controller.nama.isNotEmpty
? controller.nama
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 30,
),
)
: null,
),
@ -396,6 +405,16 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
Get.toNamed('/profile');
},
),
const Divider(),
_buildMenuItem(
icon: Icons.info_outline,
activeIcon: Icons.info,
title: 'Tentang Kami',
onTap: () {
Navigator.pop(context);
Get.toNamed('/about');
},
),
_buildMenuItem(
icon: Icons.logout,
title: 'Keluar',
@ -411,7 +430,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'© ${DateTime.now().year} Aplikasi Penyaluran Bantuan',
'© ${DateTime.now().year} DisalurKita',
style: TextStyle(
fontSize: 12,
color: Colors.grey,

View File

@ -43,7 +43,7 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
// Tambahkan widget untuk menampilkan waktu terakhir update
Widget _buildLastUpdateInfo(BuildContext context) {
final lastUpdate = DateTime.now();
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
return Padding(
padding: const EdgeInsets.only(top: 8.0),
@ -135,7 +135,7 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
),
),
Text(
'${DateTimeHelper.formatNumber(filteredPengaduan.length)} item',
'${FormatHelper.formatNumber(filteredPengaduan.length)} item',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
@ -154,9 +154,9 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
// Format tanggal menggunakan DateTimeHelper
String formattedDate = '';
if (item.tanggalPengaduan != null) {
formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan);
formattedDate = FormatHelper.formatDateTime(item.tanggalPengaduan);
} else if (item.createdAt != null) {
formattedDate = DateTimeHelper.formatDate(item.createdAt);
formattedDate = FormatHelper.formatDateTime(item.createdAt);
}
Color statusColor = AppTheme.successColor;

View File

@ -4,6 +4,7 @@ import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
const RiwayatPenitipanView({super.key});
@ -47,7 +48,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? '';
final deskripsi = item.deskripsi?.toLowerCase() ?? '';
final tanggal =
DateTimeHelper.formatDateTime(item.tanggalPenitipan).toLowerCase();
FormatHelper.formatDateTime(item.tanggalPenitipan).toLowerCase();
return donaturNama.contains(searchText) ||
kategoriNama.contains(searchText) ||
@ -99,7 +100,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
),
),
Text(
'${DateTimeHelper.formatNumber(filteredList.length)} item',
'${FormatHelper.formatNumber(filteredList.length)} item',
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
@ -113,7 +114,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total: ${DateTimeHelper.formatNumber(filteredList.length)} item',
'Total: ${FormatHelper.formatNumber(filteredList.length)} item',
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
@ -126,7 +127,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
size: 16, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'Update: ${DateTimeHelper.formatDateTimeWithHour(controller.lastUpdateTime.value)}',
'Update: ${FormatHelper.formatDateTimeWithHour(controller.lastUpdateTime.value)}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
@ -262,7 +263,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
],
),
Text(
DateTimeHelper.formatDate(item.createdAt),
FormatHelper.formatDateTime(item.createdAt),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey.shade700,
fontStyle: FontStyle.italic,
@ -282,17 +283,26 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
Row(
children: [
CircleAvatar(
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
radius: 20,
child: Text(
donaturNama.isNotEmpty
? donaturNama.substring(0, 1).toUpperCase()
: '?',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
),
),
backgroundColor: statusColor.withOpacity(0.2),
backgroundImage: item.donatur != null &&
item.donatur!.fotoProfil != null &&
item.donatur!.fotoProfil!.isNotEmpty
? NetworkImage(item.donatur!.fotoProfil!)
: null,
child: (item.donatur == null ||
item.donatur!.fotoProfil == null ||
item.donatur!.fotoProfil!.isEmpty)
? Text(
donaturNama.isNotEmpty
? donaturNama.substring(0, 1).toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: statusColor,
),
)
: null,
),
const SizedBox(width: 12),
Expanded(
@ -422,8 +432,8 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
const SizedBox(height: 4),
Text(
isUang
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan',
? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
: '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan',
style: Theme.of(context)
.textTheme
.titleSmall
@ -579,20 +589,20 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
_buildDetailItem(
'Jumlah',
isUang
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan'),
? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
: '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan'),
if (isUang) _buildDetailItem('Jenis Bantuan', 'Uang (Rupiah)'),
_buildDetailItem(
'Deskripsi', item.deskripsi ?? 'Tidak ada deskripsi'),
_buildDetailItem(
'Tanggal Penitipan',
DateTimeHelper.formatDateTime(item.tanggalPenitipan,
FormatHelper.formatDateTime(item.tanggalPenitipan,
defaultValue: 'Tidak ada tanggal'),
),
if (item.tanggalVerifikasi != null)
_buildDetailItem(
'Tanggal Verifikasi',
DateTimeHelper.formatDateTime(item.tanggalVerifikasi),
FormatHelper.formatDateTime(item.tanggalVerifikasi),
),
if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null)
_buildDetailItem(
@ -600,7 +610,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
controller.getPetugasDesaNama(item.petugasDesaId),
),
_buildDetailItem('Tanggal Dibuat',
DateTimeHelper.formatDateTime(item.createdAt)),
FormatHelper.formatDateTime(item.createdAt)),
if (item.alasanPenolakan != null &&
item.alasanPenolakan!.isNotEmpty)
_buildDetailItem('Alasan Penolakan', item.alasanPenolakan!),
@ -626,8 +636,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_showFullScreenImage(
context, item.fotoBantuan![index]);
ShowImageDialog.show(
context,
item.fotoBantuan![index],
);
},
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
@ -677,8 +689,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_showFullScreenImage(
context, item.fotoBantuan![index]);
ShowImageDialog.show(
context,
item.fotoBantuan![index],
);
},
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
@ -721,8 +735,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
const SizedBox(height: 8),
GestureDetector(
onTap: () {
_showFullScreenImage(
context, item.fotoBuktiSerahTerima!);
ShowImageDialog.show(
context,
item.fotoBuktiSerahTerima!,
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
@ -757,58 +773,6 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
);
}
void _showFullScreenImage(BuildContext context, String imageUrl) {
Get.dialog(
Dialog(
insetPadding: EdgeInsets.zero,
child: Stack(
fit: StackFit.expand,
children: [
InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 4,
child: Image.network(
imageUrl,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey.shade300,
child: const Center(
child: Icon(
Icons.error,
size: 50,
color: Colors.red,
),
),
);
},
),
),
Positioned(
top: 20,
right: 20,
child: GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
color: Colors.white,
),
),
),
),
],
),
),
);
}
Widget _buildDetailItem(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),

View File

@ -52,7 +52,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
.getKategoriBantuanName(item.kategoriBantuanId)
.toLowerCase();
final tanggal =
DateTimeHelper.formatDateTime(item.tanggalPenyaluran).toLowerCase();
FormatHelper.formatDateTime(item.tanggalPenyaluran).toLowerCase();
return nama.contains(searchText) ||
deskripsi.contains(searchText) ||
@ -105,7 +105,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
),
),
Text(
'${DateTimeHelper.formatNumber(filteredList.length)} item',
'${FormatHelper.formatNumber(filteredList.length)} item',
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
@ -119,7 +119,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total: ${DateTimeHelper.formatNumber(filteredList.length)} item',
'Total: ${FormatHelper.formatNumber(filteredList.length)} item',
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
@ -132,7 +132,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
size: 16, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'Update: ${DateTimeHelper.formatDateTimeWithHour(DateTime.now())}',
'Update: ${FormatHelper.formatDateTimeWithHour(DateTime.now())}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
@ -305,7 +305,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
child: _buildInfoItem(
Icons.event,
'Tanggal',
DateTimeHelper.formatDateTime(item.tanggalPenyaluran,
FormatHelper.formatDateTime(item.tanggalPenyaluran,
format: 'dd MMM yyyy HH:mm'),
Theme.of(context).textTheme,
),
@ -316,17 +316,57 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
_buildInfoItem(
Icons.people_outline,
'Jumlah Penerima',
'${DateTimeHelper.formatNumber(item.jumlahPenerima ?? 0)} orang',
'${FormatHelper.formatNumber(item.jumlahPenerima ?? 0)} orang',
Theme.of(context).textTheme,
),
if (item.alasanPembatalan != null &&
item.alasanPembatalan!.isNotEmpty) ...[
const SizedBox(height: 8),
_buildInfoItem(
Icons.info_outline,
'Alasan Pembatalan',
item.alasanPembatalan!,
Theme.of(context).textTheme,
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.cancel_outlined,
size: 20,
color: Colors.red.shade700,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Alasan Pembatalan',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.red.shade700,
),
),
const SizedBox(height: 4),
Text(
item.alasanPembatalan!,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Colors.red.shade800,
),
),
],
),
),
],
),
),
],
const SizedBox(height: 16),

View File

@ -6,6 +6,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_stok
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class RiwayatStokView extends GetView<RiwayatStokController> {
const RiwayatStokView({super.key});
@ -353,7 +354,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
overflow: TextOverflow.ellipsis,
),
);
}).toList(),
}),
],
onChanged: (value) {
if (value != null) {
@ -543,7 +544,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
const SizedBox(height: 4),
Text(
riwayat.createdAt != null
? DateTimeHelper.formatDateTime(
? FormatHelper.formatDateTime(
riwayat.createdAt!)
: '-',
style: TextStyle(
@ -598,7 +599,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
padding: const EdgeInsets.only(left: 44),
child: InkWell(
onTap: () =>
_showImageDialog(context, riwayat.fotoBukti!),
ShowImageDialog.show(context, riwayat.fotoBukti!),
child: Container(
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
@ -704,97 +705,6 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
);
}
void _showImageDialog(BuildContext context, String imageUrl) {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
insetPadding: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AppBar(
leading: IconButton(
icon: const Icon(
Icons.close,
color: Colors.white,
),
onPressed: () => Navigator.of(context).pop(),
),
title: const Text(
'Bukti Foto',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
elevation: 0,
backgroundColor: AppTheme.primaryColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.5,
child: InteractiveViewer(
panEnabled: true,
boundaryMargin: const EdgeInsets.all(16),
minScale: 0.5,
maxScale: 4,
child: CachedNetworkImage(
imageUrl: imageUrl,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, color: Colors.red, size: 48),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Gagal memuat gambar: $error',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
),
],
),
fit: BoxFit.contain,
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.zoom_in, size: 20, color: Colors.grey),
const SizedBox(width: 8),
Text(
'Cubit untuk memperbesar/memperkecil',
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
],
),
),
],
),
);
},
);
}
void _showStokManualDialog(BuildContext context, {required bool isAddition}) {
// Reset form
controller.resetForm();
@ -1152,7 +1062,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
Widget _buildPenitipanDetail(
BuildContext context, Map<String, dynamic> data) {
final String tanggal = data['created_at'] != null
? DateTimeHelper.formatDateTime(DateTime.parse(data['created_at']))
? FormatHelper.formatDateTime(DateTime.parse(data['created_at']))
: '-';
final String namaPenitip = data['donatur'] != null
@ -1357,7 +1267,8 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
padding: EdgeInsets.only(
right: index < fotoBantuan.length - 1 ? 8.0 : 0),
child: InkWell(
onTap: () => _showImageDialog(context, imageUrl),
onTap: () =>
ShowImageDialog.show(context, imageUrl),
child: Container(
width: 200,
decoration: BoxDecoration(
@ -1442,7 +1353,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
Widget _buildPenerimaanDetail(
BuildContext context, Map<String, dynamic> data) {
final String tanggal = data['created_at'] != null
? DateTimeHelper.formatDateTime(DateTime.parse(data['created_at']))
? FormatHelper.formatDateTime(DateTime.parse(data['created_at']))
: '-';
final String namaPenerima = data['warga'] != null
@ -1646,7 +1557,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
),
const SizedBox(height: 12),
InkWell(
onTap: () => _showImageDialog(context, buktiPenerimaan),
onTap: () => ShowImageDialog.show(context, buktiPenerimaan),
child: Container(
height: 180,
width: double.infinity,

View File

@ -156,7 +156,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
),
),
Text(
'Rp ${DateTimeHelper.formatNumber(controller.totalDanaBantuan.value)}',
'Rp ${FormatHelper.formatNumber(controller.totalDanaBantuan.value)}',
style:
Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
@ -512,8 +512,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
),
Text(
item.isUang == true
? 'Rp ${DateTimeHelper.formatNumber(item.totalStok)}'
: '${DateTimeHelper.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
? 'Rp ${FormatHelper.formatNumber(item.totalStok)}'
: '${FormatHelper.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
style: Theme.of(context)
.textTheme
.titleLarge
@ -549,7 +549,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
Expanded(
child: Text(
item.updatedAt != null
? 'Diperbarui: ${DateTimeHelper.formatDateTimeWithHour(item.updatedAt!)}'
? 'Diperbarui: ${FormatHelper.formatDateTimeWithHour(item.updatedAt!)}'
: 'Tidak ada data pembaruan',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
@ -984,8 +984,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
const SizedBox(width: 8),
Text(
isUang
? 'Rp ${DateTimeHelper.formatNumber(stok.totalStok)}'
: '${DateTimeHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
? 'Rp ${FormatHelper.formatNumber(stok.totalStok)}'
: '${FormatHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
@ -1175,8 +1175,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
SizedBox(width: 4),
Text(
stok.isUang == true
? 'Rp ${DateTimeHelper.formatNumber(stok.totalStok)}'
: '${DateTimeHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
? 'Rp ${FormatHelper.formatNumber(stok.totalStok)}'
: '${FormatHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
@ -1240,7 +1240,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
Widget _buildLastUpdateInfo(BuildContext context) {
return Obx(() {
final lastUpdate = controller.lastUpdateTime.value;
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
return Padding(
padding: const EdgeInsets.only(top: 8.0),

View File

@ -0,0 +1,233 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:uuid/uuid.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
class TambahLokasiPenyaluranView extends GetView<JadwalPenyaluranController> {
const TambahLokasiPenyaluranView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Tambah Lokasi Penyaluran'),
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
body: _buildTambahLokasiPenyaluranForm(context),
);
}
Widget _buildTambahLokasiPenyaluranForm(BuildContext context) {
final formKey = GlobalKey<FormState>();
final TextEditingController namaController = TextEditingController();
final TextEditingController alamatLengkapController =
TextEditingController();
return Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Judul Form
Text(
'Formulir Lokasi Penyaluran',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// Nama Lokasi
Text(
'Nama Lokasi',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
TextFormField(
controller: namaController,
decoration: InputDecoration(
hintText: 'Masukkan nama lokasi penyaluran',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nama lokasi tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 16),
// Alamat Lengkap
Text(
'Alamat Lengkap',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
TextFormField(
controller: alamatLengkapController,
maxLines: 3,
decoration: InputDecoration(
hintText: 'Masukkan alamat lengkap lokasi',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Alamat lengkap tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 24),
// Tombol Submit
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
// Panggil fungsi untuk menambahkan lokasi penyaluran
_tambahLokasiPenyaluran(
nama: namaController.text,
alamatLengkap: alamatLengkapController.text,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Simpan Lokasi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
);
}
Future<void> _tambahLokasiPenyaluran({
required String nama,
required String alamatLengkap,
}) async {
try {
// Tampilkan loading
Get.dialog(
const Center(
child: CircularProgressIndicator(),
),
barrierDismissible: false,
);
// Generate UUID untuk ID lokasi
final uuid = const Uuid();
final String id = uuid.v4();
// Ambil ID petugas desa yang sedang login dari controller
final String? petugasDesaId = controller.supabaseService.currentUser?.id;
if (petugasDesaId == null) {
Get.back(); // Tutup dialog loading
ScaffoldMessenger.of(Get.context!).showSnackBar(
const SnackBar(
content: Text('Sesi login tidak valid. Silakan login kembali.'),
backgroundColor: Colors.red,
),
);
return;
}
// Dapatkan desa_id dari data petugas desa
// Ambil data petugas desa dari Supabase untuk mendapatkan desa_id
final petugasDesaData = await controller.supabaseService.client
.from('petugas_desa')
.select('desa_id')
.eq('id', petugasDesaId)
.single();
final String? desaId = petugasDesaData['desa_id'];
if (desaId == null) {
Get.back(); // Tutup dialog loading
ScaffoldMessenger.of(Get.context!).showSnackBar(
const SnackBar(
content: Text(
'Data desa tidak ditemukan. Silakan hubungi administrator.'),
backgroundColor: Colors.red,
),
);
return;
}
// Data untuk insert
final Map<String, dynamic> data = {
'id': id,
'nama': nama,
'alamat_lengkap': alamatLengkap,
'desa_id': desaId,
'created_at': DateTime.now().toIso8601String(),
};
// Insert data ke tabel lokasi_penyaluran
await controller.supabaseService.client
.from('lokasi_penyaluran')
.insert(data);
// Tutup dialog loading
Get.back();
// Tampilkan pesan sukses
ScaffoldMessenger.of(Get.context!).showSnackBar(
const SnackBar(
content: Text('Lokasi penyaluran berhasil ditambahkan'),
backgroundColor: Colors.green,
),
);
// Kembali ke halaman sebelumnya
Get.back();
// Refresh data di controller
controller.refreshData();
} catch (e) {
// Tutup dialog loading
Get.back();
// Tampilkan pesan error
ScaffoldMessenger.of(Get.context!).showSnackBar(
SnackBar(
content: Text('Gagal menambahkan lokasi penyaluran: $e'),
backgroundColor: Colors.red,
),
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -231,12 +231,39 @@ class ProfileView extends GetView<ProfileController> {
Widget _buildDefaultProfileImage() {
return CircleAvatar(
radius: 60,
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
child: const Icon(
Icons.person,
size: 70,
color: AppTheme.primaryColor,
),
backgroundColor: AppTheme.primaryColor.withOpacity(0.2),
child: Obx(() {
final user = controller.user.value;
final roleData = controller.roleData.value;
String displayInitial = '?';
if (roleData != null && roleData.isNotEmpty) {
final roleDataValue = roleData;
if (roleDataValue['nama_lengkap'] != null &&
roleDataValue['nama_lengkap'].toString().isNotEmpty) {
displayInitial = roleDataValue['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase();
} else if (roleDataValue['nama'] != null &&
roleDataValue['nama'].toString().isNotEmpty) {
displayInitial =
roleDataValue['nama'].toString().substring(0, 1).toUpperCase();
}
} else if (user != null && user.name != null && user.name!.isNotEmpty) {
displayInitial = user.name!.substring(0, 1).toUpperCase();
}
return Text(
displayInitial,
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 60,
),
);
}),
);
}

View File

@ -33,23 +33,20 @@ class _SplashViewState extends State<SplashView> {
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/logo.png',
width: 120,
height: 120,
'assets/images/logo-disalurkita.png',
width: 150,
height: 150,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Colors.white,
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
@ -62,24 +59,25 @@ class _SplashViewState extends State<SplashView> {
),
const SizedBox(height: 24),
const Text(
'Aplikasi Penyaluran',
'DisalurKita',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 8),
const Text(
'Bantuan Sosial',
'Salurkan dengan Pasti, Pantau dengan Bukti',
style: TextStyle(
fontSize: 18,
color: Colors.white,
fontSize: 16,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 48),
const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
valueColor:
AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
),
],
),

View File

@ -1,16 +1,16 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:timeline_tile/timeline_tile.dart';
import 'package:image_picker/image_picker.dart';
import 'package:penyaluran_app/app/widgets/indicators/status_pill.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart';
import 'package:penyaluran_app/app/widgets/cards/info_card.dart';
import 'dart:io';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
const WargaDetailPengaduanView({super.key});
@ -670,8 +670,7 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
const SizedBox(width: 12),
Text(
pengaduan.tanggalPengaduan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(pengaduan.tanggalPengaduan!)
? FormatHelper.formatDateTime(pengaduan.tanggalPengaduan!)
: '-',
style: TextStyle(
fontSize: 15,
@ -1309,8 +1308,8 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
child: Row(
children: tindakan.buktiTindakan!.map((bukti) {
return GestureDetector(
onTap: () =>
showFullScreenImage(context, bukti),
onTap: () => ShowImageDialog.showFullScreen(
context, bukti),
child: Container(
width: 100,
height: 100,
@ -1407,8 +1406,8 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
Expanded(
child: Text(
tindakan.tanggalTindakan != null
? DateFormat('dd MMM yyyy HH:mm', 'id_ID')
.format(tindakan.tanggalTindakan!)
? FormatHelper.formatDateTime(
tindakan.tanggalTindakan!)
: '-',
style: TextStyle(
fontSize: 12,
@ -1429,183 +1428,8 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
);
}
void showFullScreenImage(BuildContext context, String imageUrl) {
// Buat controller untuk InteractiveViewer
final TransformationController transformationController =
TransformationController();
Get.dialog(
Dialog(
insetPadding: EdgeInsets.zero,
child: Stack(
fit: StackFit.expand,
children: [
Container(
color: Colors.black,
child: InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 4,
transformationController: transformationController,
child: Center(
child: Hero(
tag: imageUrl,
child: imageUrl.startsWith('http')
? Image.network(
imageUrl,
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white),
value: loadingProgress.expectedTotalBytes !=
null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.broken_image,
size: 60,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
'Gagal memuat gambar',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
)
: Image.file(
File(imageUrl),
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.broken_image,
size: 60,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
'Gagal memuat gambar',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
),
),
),
),
),
Positioned(
top: 20,
right: 20,
child: GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
color: Colors.white,
size: 24,
),
),
),
),
Positioned(
bottom: 20,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildImageControlButton(
icon: Icons.zoom_out,
onTap: () {
// Zoom out
final Matrix4 matrix =
transformationController.value.clone();
matrix.scale(0.75);
transformationController.value = matrix;
},
),
const SizedBox(width: 16),
_buildImageControlButton(
icon: Icons.refresh,
onTap: () {
// Reset
transformationController.value = Matrix4.identity();
},
),
const SizedBox(width: 16),
_buildImageControlButton(
icon: Icons.zoom_in,
onTap: () {
// Zoom in
final Matrix4 matrix =
transformationController.value.clone();
matrix.scale(1.5);
transformationController.value = matrix;
},
),
],
),
),
],
),
),
);
}
Widget _buildImageControlButton({
required IconData icon,
required Function() onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Colors.white,
size: 24,
),
),
);
void _showFullScreenImage(BuildContext context, String imagePath) {
ShowImageDialog.showFullScreen(context, imagePath);
}
}
@ -2056,8 +1880,7 @@ class _TambahTindakanPengaduanViewState
}
void _showFullScreenImage(BuildContext context, String imagePath) {
final wargaDetailView = Get.find<WargaDetailPengaduanView>();
wargaDetailView.showFullScreenImage(context, imagePath);
ShowImageDialog.showFullScreen(context, imagePath);
}
Future<void> _simpanTindakan() async {
@ -2078,22 +1901,6 @@ class _TambahTindakanPengaduanViewState
});
try {
// Di sini kita baru melakukan upload file ke server
// Contoh implementasi:
// 1. Upload semua file bukti tindakan
// final List<String> buktiTindakanUrls = await uploadMultipleFiles(buktiTindakanPaths);
// 2. Simpan data tindakan ke database
// await saveTindakanPengaduan(
// pengaduanId: widget.pengaduanId,
// kategoriTindakan: selectedKategori!,
// prioritas: selectedPrioritas!,
// tindakan: tindakanController.text,
// catatan: catatanController.text,
// buktiTindakanUrls: buktiTindakanUrls,
// );
// Tampilkan pesan sukses
Get.back(); // Kembali ke halaman sebelumnya
Get.snackbar(

View File

@ -11,12 +11,12 @@ class FormPengaduanView extends StatefulWidget {
final List<File>? selectedImages;
const FormPengaduanView({
Key? key,
super.key,
required this.uidPenerimaan,
this.judul,
this.deskripsi,
this.selectedImages,
}) : super(key: key);
});
@override
State<FormPengaduanView> createState() => _FormPengaduanViewState();
@ -219,7 +219,7 @@ class _FormPengaduanViewState extends State<FormPengaduanView> {
),
),
const SizedBox(height: 8),
Container(
SizedBox(
height: 120,
child: ListView.builder(
scrollDirection: Axis.horizontal,

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart';
class WargaDashboardView extends GetView<WargaDashboardController> {
@ -23,6 +23,54 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header DisalurKita dengan logo dan slogan
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Image.asset(
'assets/images/logo-disalurkita.png',
width: 50,
height: 50,
),
const SizedBox(width: 15),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'DisalurKita',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF1565C0),
),
),
SizedBox(height: 5),
Text(
'Salurkan dengan Pasti, Pantau dengan Bukti',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
_buildWelcomeSection(),
const SizedBox(height: 24),
_buildStatisticSection(),
@ -90,10 +138,17 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
? NetworkImage(controller.profilePhotoUrl!)
: null,
child: controller.profilePhotoUrl == null
? Icon(
Icons.person,
color: Colors.blue.shade700,
size: 30,
? Text(
controller.nama.isNotEmpty
? controller.nama
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
fontSize: 24,
),
)
: null,
),
@ -417,12 +472,6 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
}
Widget _buildPenerimaanSummary() {
final currencyFormat = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
double totalUang = 0;
Map<String, double> totalNonUang = {};
@ -494,7 +543,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
icon: Icons.attach_money,
color: Colors.green,
title: 'Total Bantuan Uang',
value: currencyFormat.format(totalUang),
value: FormatHelper.formatRupiah(totalUang),
),
if (totalNonUang.isNotEmpty) ...[
if (totalUang > 0)

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/status_badge.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:image_picker/image_picker.dart';
@ -131,17 +131,11 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
}
Widget _buildHeaderSection(PenerimaPenyaluranModel penyaluran) {
final currencyFormat = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
// Format jumlah bantuan berdasarkan tipe (uang atau bukan)
String formattedJumlah = '';
if (penyaluran.jumlahBantuan != null) {
if (penyaluran.isUang == true) {
formattedJumlah = currencyFormat.format(penyaluran.jumlahBantuan);
formattedJumlah = FormatHelper.formatRupiah(penyaluran.jumlahBantuan);
} else {
formattedJumlah =
'${penyaluran.jumlahBantuan} ${penyaluran.satuan ?? ''}';
@ -390,8 +384,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
icon: Icons.calendar_today,
title: 'Tanggal Penerimaan',
value: penyaluran.tanggalPenerimaan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(penyaluran.tanggalPenerimaan!)
? FormatHelper.formatDateTime(penyaluran.tanggalPenerimaan!)
: 'Belum diterima',
statusColor: null,
),
@ -400,8 +393,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
icon: Icons.access_time,
title: 'Waktu Penerimaan',
value: penyaluran.tanggalPenerimaan != null
? DateFormat('HH:mm', 'id_ID')
.format(penyaluran.tanggalPenerimaan!)
? FormatHelper.formatDateTime(penyaluran.tanggalPenerimaan!)
: 'Belum diterima',
statusColor: null,
),
@ -758,8 +750,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
icon: Icons.update,
title: 'Terakhir Diperbarui',
value: penyaluran.tanggalPenerimaan != null
? DateFormat('dd MMMM yyyy HH:mm', 'id_ID')
.format(penyaluran.tanggalPenerimaan!)
? FormatHelper.formatDateTime(penyaluran.tanggalPenerimaan!)
: 'Tidak tersedia',
statusColor: null,
),
@ -1394,7 +1385,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
),
const SizedBox(width: 8),
const Text(
'Pengaduan Terdaftar',
'Pengaduan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -1547,8 +1538,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
const SizedBox(width: 8),
Text(
pengaduan.tanggalPengaduan != null
? DateFormat('dd MMMM yyyy HH:mm', 'id_ID')
.format(pengaduan.tanggalPengaduan!)
? FormatHelper.formatDateTime(pengaduan.tanggalPengaduan!)
: 'Tanggal tidak tersedia',
style: TextStyle(
fontSize: 12,

View File

@ -1,12 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
import 'package:penyaluran_app/app/modules/warga/views/form_pengaduan_view.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'dart:io';
import 'package:image_picker/image_picker.dart';
class WargaPengaduanView extends GetView<WargaDashboardController> {
const WargaPengaduanView({super.key});
@ -380,7 +375,7 @@ class WargaPengaduanView extends GetView<WargaDashboardController> {
Expanded(
child: Text(
item.tanggalPengaduan != null
? DateTimeHelper.formatDateTime(
? FormatHelper.formatDateTime(
item.tanggalPengaduan!)
: '-',
style: TextStyle(

View File

@ -20,13 +20,13 @@ class WargaView extends GetView<WargaDashboardController> {
title: Obx(() {
switch (controller.activeTabIndex.value) {
case 0:
return const Text('Dashboard Warga');
return const Text('Dashboard');
case 1:
return const Text('Penerimaan Bantuan');
case 2:
return const Text('Pengaduan');
default:
return const Text('Dashboard Warga');
return const Text('Dashboard');
}
}),
leading: IconButton(
@ -164,16 +164,19 @@ class WargaView extends GetView<WargaDashboardController> {
child: CircleAvatar(
radius: 40,
backgroundColor: Colors.white70,
backgroundImage: controller.profilePhotoUrl != null &&
controller.profilePhotoUrl!.isNotEmpty
? NetworkImage(controller.profilePhotoUrl!)
backgroundImage: controller.fotoProfil.value.isNotEmpty
? NetworkImage(controller.fotoProfil.value)
: null,
child: controller.profilePhotoUrl == null ||
controller.profilePhotoUrl!.isEmpty
? Icon(
Icons.person,
color: Colors.white,
size: 40,
child: controller.fotoProfil.isEmpty
? Text(
controller.nama.isNotEmpty
? controller.nama.substring(0, 1).toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 24,
),
)
: null,
),
@ -292,6 +295,15 @@ class WargaView extends GetView<WargaDashboardController> {
controller.refreshData();
},
),
_buildMenuItem(
icon: Icons.info_outline,
activeIcon: Icons.info,
title: 'Tentang Kami',
onTap: () {
Navigator.pop(context);
Get.toNamed('/about');
},
),
_buildMenuItem(
icon: Icons.logout,
title: 'Keluar',
@ -307,7 +319,7 @@ class WargaView extends GetView<WargaDashboardController> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'© ${DateTime.now().year} Aplikasi Penyaluran Bantuan',
'© ${DateTime.now().year} DisalurKita',
style: TextStyle(
fontSize: 12,
color: Colors.grey,