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

@ -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