membuat tampilan lebih menarik

This commit is contained in:
Khafidh Fuadi
2025-03-27 22:31:14 +07:00
parent f6d3eef2cf
commit c008020705
44 changed files with 6260 additions and 3195 deletions

View File

@ -13,7 +13,7 @@ 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/date_time_helper.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
class LaporanPenyaluranController extends GetxController {
final AuthController _authController = Get.find<AuthController>();

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.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/section_header.dart';
import 'dart:io';
@ -370,6 +370,9 @@ class LaporanPenyaluranCreateView extends GetView<LaporanPenyaluranController> {
return;
}
controller.saveLaporan(penyaluranId);
//kembali reload halaman
// Kembali dan reload halaman setelah menyimpan laporan
Get.back(result: true);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.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/section_header.dart';
import 'package:penyaluran_app/app/widgets/status_badge.dart';

View File

@ -3,9 +3,8 @@ import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/laporan_penyaluran_model.dart';
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.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/section_header.dart';
import 'package:penyaluran_app/app/widgets/status_badge.dart';
import 'package:intl/intl.dart';
@ -34,36 +33,58 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
if (controller.daftarLaporan.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.note_alt_outlined,
size: 64, color: Colors.grey),
const SizedBox(height: 16),
const Text(
'Belum ada laporan penyaluran',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.shade50,
shape: BoxShape.circle,
),
child: Icon(
Icons.description_outlined,
size: 72,
color: AppTheme.primaryColor,
),
),
),
const SizedBox(height: 8),
const Text(
'Buat laporan baru untuk penyaluran yang telah selesai',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () => _showPenyaluranDialog(context),
icon: const Icon(Icons.add),
label: const Text('Buat Laporan Baru'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 12),
const SizedBox(height: 24),
const Text(
'Belum Ada Laporan Penyaluran',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
const SizedBox(height: 12),
const Text(
'Buat laporan baru untuk penyaluran bantuan yang telah selesai',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.grey,
height: 1.5,
),
),
const SizedBox(height: 32),
ElevatedButton.icon(
onPressed: () => _showPenyaluranDialog(context),
icon: const Icon(Icons.add),
label: const Text('Buat Laporan Baru'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
],
),
),
);
}
@ -110,24 +131,42 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
// Widget untuk filter status
Widget _buildStatusFilter() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
color: AppTheme.primaryColor.withOpacity(0.05),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SectionHeader(
title: 'Filter Status',
// subtitle: 'Tampilkan laporan berdasarkan status',
// showDivider: false,
Row(
children: [
Icon(Icons.filter_list, color: AppTheme.primaryColor),
const SizedBox(width: 8),
const Text(
'Filter Status Laporan',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
const SizedBox(height: 8),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildFilterChip('SEMUA'),
_buildFilterChip('DRAFT'),
_buildFilterChip('FINAL'),
_buildFilterChip('SEMUA', Icons.list_alt),
_buildFilterChip('DRAFT', Icons.edit_note),
_buildFilterChip('FINAL', Icons.check_circle),
],
),
),
@ -137,23 +176,42 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
}
// Chip untuk filter
Widget _buildFilterChip(String status) {
Widget _buildFilterChip(String status, IconData icon) {
return Obx(() {
final isSelected = controller.filterStatus.value == status;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
selected: isSelected,
label: Text(status),
onSelected: (_) {
controller.filterStatus.value = status;
},
backgroundColor: Colors.white,
checkmarkColor: Colors.white,
selectedColor: AppTheme.primaryColor,
labelStyle: TextStyle(
color: isSelected ? Colors.white : Colors.black,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
return Container(
margin: const EdgeInsets.only(right: 12),
child: Material(
elevation: isSelected ? 2 : 0,
borderRadius: BorderRadius.circular(25),
color: isSelected ? AppTheme.primaryColor : Colors.grey.shade100,
child: InkWell(
onTap: () {
controller.filterStatus.value = status;
},
borderRadius: BorderRadius.circular(25),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Icon(
icon,
size: 16,
color: isSelected ? Colors.white : Colors.black54,
),
const SizedBox(width: 6),
Text(
status,
style: TextStyle(
color: isSelected ? Colors.white : Colors.black87,
fontWeight:
isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 13,
),
),
],
),
),
),
),
);
@ -165,71 +223,77 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
BuildContext context, LaporanPenyaluranModel laporan) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 3,
elevation: 2,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(16),
),
child: InkWell(
onTap: () {
Get.toNamed('/laporan-penyaluran/detail', arguments: laporan.id);
},
borderRadius: BorderRadius.circular(12),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: AppTheme.primaryGradient,
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
laporan.judul,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.white,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
StatusBadge(status: laporan.status ?? 'DRAFT'),
],
),
const Divider(height: 24, color: Colors.white30),
Row(
children: [
Icon(
Icons.calendar_today,
size: 16,
color: Colors.white,
),
const SizedBox(width: 6),
Text(
'Tanggal: ${laporan.tanggalLaporan != null ? DateTimeHelper.formatDateTime(laporan.tanggalLaporan!) : '-'}',
style: TextStyle(
fontSize: 12,
child: Column(
children: [
// Header dengan status
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
laporan.judul,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.white,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (laporan.status == 'FINAL')
Material(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
child: InkWell(
borderRadius: BorderRadius.circular(8),
),
const SizedBox(width: 8),
StatusBadge(status: laporan.status ?? 'DRAFT'),
],
),
),
// Body with details
Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Informasi dengan icon
Row(
children: [
_buildInfoItem(
Icons.calendar_today,
'Tanggal',
laporan.tanggalLaporan != null
? DateTimeHelper.formatDateTime(
laporan.tanggalLaporan!)
: '-',
),
_buildInfoItem(
Icons.description,
'Status',
laporan.status ?? 'DRAFT',
),
],
),
const SizedBox(height: 16),
// Action buttons
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (laporan.status == 'FINAL')
_buildActionButton(
icon: Icons.picture_as_pdf,
label: 'Ekspor PDF',
color: Colors.blue,
onTap: () {
controller
.fetchPenyaluranDetail(
@ -241,85 +305,104 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
}
});
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
child: Row(
children: const [
Icon(
Icons.picture_as_pdf,
size: 16,
color: Colors.white,
),
SizedBox(width: 4),
Text(
'Ekspor PDF',
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
const SizedBox(width: 8),
Material(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
child: InkWell(
borderRadius: BorderRadius.circular(8),
const SizedBox(width: 8),
_buildActionButton(
icon: Icons.edit,
label: 'Edit',
color: Colors.orange,
onTap: () {
Get.toNamed('/laporan-penyaluran/edit',
arguments: laporan.id);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
child: Row(
children: const [
Icon(Icons.edit, color: Colors.orange, size: 16),
SizedBox(width: 4),
Text('Edit',
style: TextStyle(
color: Colors.orange,
fontWeight: FontWeight.bold)),
],
),
),
),
),
const SizedBox(width: 8),
Material(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
child: InkWell(
borderRadius: BorderRadius.circular(8),
const SizedBox(width: 8),
_buildActionButton(
icon: Icons.delete,
label: 'Hapus',
color: Colors.red,
onTap: () {
_showDeleteConfirmation(context, laporan.id!);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
child: Row(
children: const [
Icon(Icons.delete, color: Colors.red, size: 16),
SizedBox(width: 4),
Text('Hapus',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold)),
],
),
),
),
),
],
],
),
],
),
),
],
),
),
);
}
// Helper untuk item informasi
Widget _buildInfoItem(IconData icon, String label, String value) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 16, color: AppTheme.primaryColor),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade700,
),
),
],
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
],
),
),
);
}
// Helper untuk tombol aksi
Widget _buildActionButton({
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
return Material(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Row(
children: [
Icon(icon, color: color, size: 16),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
),
@ -332,19 +415,42 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Hapus Laporan'),
content: const Text('Apakah Anda yakin ingin menghapus laporan ini?'),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Icon(Icons.warning_amber_rounded, color: Colors.red, size: 24),
const SizedBox(width: 8),
const Text('Hapus Laporan'),
],
),
content: const Text(
'Apakah Anda yakin ingin menghapus laporan ini? Tindakan ini tidak dapat dibatalkan.',
style: TextStyle(height: 1.5),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Batal'),
),
TextButton(
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
controller.deleteLaporan(laporanId);
controller.deleteLaporan(laporanId).then((_) {
Get.snackbar(
'Berhasil',
'Laporan berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
});
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Hapus'),
),
],
@ -370,31 +476,90 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Pilih Penyaluran'),
title: Row(
children: [
Icon(Icons.assignment, color: AppTheme.primaryColor),
const SizedBox(width: 8),
const Text('Pilih Penyaluran'),
],
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
contentPadding: const EdgeInsets.only(top: 16, left: 24, right: 24),
content: SizedBox(
width: double.maxFinite,
child: ListView.builder(
shrinkWrap: true,
itemCount: controller.penyaluranTanpaLaporan.length,
itemBuilder: (context, index) {
final penyaluran = controller.penyaluranTanpaLaporan[index];
return ListTile(
title: Text(
penyaluran.nama ??
'Penyaluran #${penyaluran.id?.substring(0, 8)}',
style: const TextStyle(fontWeight: FontWeight.bold),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pilih salah satu penyaluran bantuan yang akan dibuat laporannya:',
style: TextStyle(color: Colors.grey.shade700, fontSize: 13),
),
const SizedBox(height: 8),
Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.4,
),
subtitle: Text(
'Tanggal: ${penyaluran.tanggalSelesai != null ? DateFormat('dd/MM/yyyy').format(penyaluran.tanggalSelesai!) : '-'}',
child: ListView.separated(
shrinkWrap: true,
itemCount: controller.penyaluranTanpaLaporan.length,
separatorBuilder: (context, index) =>
const Divider(height: 1),
itemBuilder: (context, index) {
final penyaluran =
controller.penyaluranTanpaLaporan[index];
return ListTile(
contentPadding: const EdgeInsets.symmetric(
vertical: 6,
horizontal: 8,
),
leading: CircleAvatar(
backgroundColor:
AppTheme.primaryColor.withOpacity(0.2),
child: const Icon(
Icons.inventory_2,
color: AppTheme.primaryColor,
),
),
title: Text(
penyaluran.nama ??
'Penyaluran #${penyaluran.id?.substring(0, 8)}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Row(
children: [
Icon(
Icons.calendar_today,
size: 12,
color: Colors.grey.shade600,
),
const SizedBox(width: 4),
Text(
penyaluran.tanggalSelesai != null
? DateFormat('dd/MM/yyyy')
.format(penyaluran.tanggalSelesai!)
: '-',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () {
Navigator.of(context).pop();
// Arahkan ke halaman buat laporan dengan ID penyaluran
Get.toNamed('/laporan-penyaluran/create',
arguments: penyaluran.id);
},
);
},
),
onTap: () {
Navigator.of(context).pop();
// Arahkan ke halaman buat laporan dengan ID penyaluran
Get.toNamed('/laporan-penyaluran/create',
arguments: penyaluran.id);
},
);
},
),
],
),
),
actions: [