ambil data stok bantuan

This commit is contained in:
Khafidh Fuadi
2025-03-11 12:44:32 +07:00
parent d24832ea82
commit eec06ba79d
57 changed files with 4306 additions and 1590 deletions

View File

@ -22,9 +22,9 @@ class DashboardView extends GetView<PetugasDesaController> {
children: [
// Header dengan greeting
GreetingHeader(
name: controller.roleData.value?['namaLengkap'] ?? 'Ahmad',
name: controller.namaLengkap,
role: 'Petugas Desa',
desa: controller.roleData.value?['Desa'] ?? 'Jatihurip',
desa: controller.desa,
),
const SizedBox(height: 20),

View File

@ -1,387 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class InventarisView extends GetView<PetugasDesaController> {
const InventarisView({super.key});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Ringkasan inventaris
_buildInventarisSummary(context),
const SizedBox(height: 24),
// Filter dan pencarian
_buildFilterSearch(context),
const SizedBox(height: 20),
// Daftar inventaris
_buildInventarisList(context),
],
),
),
);
}
Widget _buildInventarisSummary(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ringkasan Inventaris',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.inventory_2_outlined,
title: 'Total Stok',
value: '1,250 kg',
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.input,
title: 'Masuk Bulan Ini',
value: '500 kg',
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.output,
title: 'Keluar Bulan Ini',
value: '350 kg',
),
),
],
),
],
),
);
}
Widget _buildSummaryItem(
BuildContext context, {
required IconData icon,
required String title,
required String value,
}) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Colors.white,
size: 24,
),
),
const SizedBox(height: 8),
Text(
value,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white,
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildFilterSearch(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Cari bantuan...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(vertical: 0),
),
),
),
const SizedBox(width: 12),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: IconButton(
onPressed: () {
// Tampilkan dialog filter
},
icon: const Icon(Icons.filter_list),
tooltip: 'Filter',
),
),
],
);
}
Widget _buildInventarisList(BuildContext context) {
final List<Map<String, dynamic>> inventarisList = [
{
'nama': 'Beras',
'jenis': 'Sembako',
'stok': '750 kg',
'lokasi': 'Gudang Utama',
'tanggal_masuk': '10 April 2023',
'kadaluarsa': '10 April 2024',
},
{
'nama': 'Minyak Goreng',
'jenis': 'Sembako',
'stok': '250 liter',
'lokasi': 'Gudang Utama',
'tanggal_masuk': '12 April 2023',
'kadaluarsa': '12 Oktober 2023',
},
{
'nama': 'Paket Sembako',
'jenis': 'Paket Bantuan',
'stok': '100 paket',
'lokasi': 'Gudang Cabang',
'tanggal_masuk': '15 April 2023',
'kadaluarsa': '15 Juli 2023',
},
{
'nama': 'Selimut',
'jenis': 'Non-Pangan',
'stok': '150 buah',
'lokasi': 'Gudang Cabang',
'tanggal_masuk': '5 April 2023',
'kadaluarsa': '-',
},
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Daftar Inventaris',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
TextButton.icon(
onPressed: () {
// Navigasi ke halaman tambah inventaris
},
icon: const Icon(Icons.add),
label: const Text('Tambah'),
style: TextButton.styleFrom(
foregroundColor: AppTheme.primaryColor,
),
),
],
),
const SizedBox(height: 12),
...inventarisList.map((item) => _buildInventarisItem(context, item)),
],
);
}
Widget _buildInventarisItem(BuildContext context, Map<String, dynamic> item) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(26),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item['nama'] ?? '',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
item['jenis'] ?? '',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.inventory,
label: 'Stok',
value: item['stok'] ?? '',
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.location_on_outlined,
label: 'Lokasi',
value: item['lokasi'] ?? '',
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.calendar_today,
label: 'Tanggal Masuk',
value: item['tanggal_masuk'] ?? '',
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.timelapse,
label: 'Kadaluarsa',
value: item['kadaluarsa'] ?? '',
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () {
// Tampilkan detail inventaris
},
icon: const Icon(Icons.edit_outlined, size: 18),
label: const Text('Edit'),
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
TextButton.icon(
onPressed: () {
// Tampilkan dialog konfirmasi hapus
},
icon: const Icon(Icons.delete_outline, size: 18),
label: const Text('Hapus'),
style: TextButton.styleFrom(
foregroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
],
),
],
),
),
);
}
Widget _buildItemDetail(
BuildContext context, {
required IconData icon,
required String label,
required String value,
}) {
return Row(
children: [
Icon(
icon,
size: 16,
color: Colors.grey,
),
const SizedBox(width: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey,
),
),
Text(
value,
style: Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
);
}
}

View File

@ -118,8 +118,8 @@ class NotifikasiView extends GetView<PetugasDesaController> {
);
},
backgroundColor: AppTheme.primaryColor,
child: const Icon(Icons.done_all),
tooltip: 'Tandai Semua Dibaca',
child: const Icon(Icons.done_all),
),
);
}

View File

@ -237,8 +237,7 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
// Daftar penerima
...daftarPenerima
.map((penerima) => _buildPenerimaItem(context, penerima))
.toList(),
.map((penerima) => _buildPenerimaItem(context, penerima)),
],
),
);

View File

@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
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/permintaan_penjadwalan_summary_widget.dart';
class PenyaluranView extends GetView<PetugasDesaController> {
class PenyaluranView extends GetView<JadwalPenyaluranController> {
const PenyaluranView({super.key});
@override

View File

@ -1,16 +1,17 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
class PermintaanPenjadwalanView extends GetView<JadwalPenyaluranController> {
const PermintaanPenjadwalanView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// Pastikan controller sudah diinisialisasi
if (!Get.isRegistered<PetugasDesaController>()) {
Get.put(PetugasDesaController());
if (!Get.isRegistered<JadwalPenyaluranController>()) {
Get.put(JadwalPenyaluranController());
}
return Scaffold(
@ -236,7 +237,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
);
}
Widget _buildPermintaanItem(BuildContext context, Map<String, dynamic> item) {
Widget _buildPermintaanItem(BuildContext context, PenyaluranBantuanModel item) {
Color statusColor = Colors.orange;
IconData statusIcon = Icons.pending_actions;
@ -265,7 +266,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
children: [
Expanded(
child: Text(
item['nama'] ?? '',
item.judul ?? '',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
@ -307,8 +308,8 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
child: _buildItemDetail(
context,
icon: Icons.person,
label: 'NIK',
value: item['nik'] ?? '',
label: 'ID',
value: item.id ?? '',
),
),
Expanded(
@ -316,7 +317,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
context,
icon: Icons.category,
label: 'Jenis Bantuan',
value: item['jenis_bantuan'] ?? '',
value: item.judul ?? '',
),
),
],
@ -329,15 +330,15 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
context,
icon: Icons.calendar_today,
label: 'Tanggal Permintaan',
value: item['tanggal_permintaan'] ?? '',
value: item.createdAt?.toString().substring(0, 10) ?? '',
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.location_on,
label: 'Alamat',
value: item['alamat'] ?? '',
label: 'Deskripsi',
value: item.deskripsi ?? '',
),
),
],
@ -420,15 +421,15 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
}
// Dialog untuk konfirmasi permintaan
void _showKonfirmasiDialog(Map<String, dynamic> permintaan) {
void _showKonfirmasiDialog(PenyaluranBantuanModel permintaan) {
String? selectedJadwalId;
// Data jadwal yang tersedia dari controller
final jadwalOptions = controller.jadwalMendatang.map((jadwal) {
return DropdownMenuItem<String>(
value: jadwal['id'],
value: jadwal.id,
child: Text(
'${jadwal['tanggal'] ?? ''} - ${jadwal['lokasi'] ?? ''} (${jadwal['jenis_bantuan'] ?? ''})'),
'${jadwal.tanggalPenjadwalan?.toString().substring(0, 10) ?? ''} - ${jadwal.lokasiPenyaluranId ?? ''} (${jadwal.judul ?? ''})'),
);
}).toList();
@ -448,7 +449,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Anda akan mengkonfirmasi permintaan penjadwalan dari ${permintaan['nama'] ?? 'Penerima'}.'),
'Anda akan mengkonfirmasi permintaan penjadwalan dari ${permintaan.judul ?? 'Penerima'}.'),
const SizedBox(height: 16),
const Text('Pilih jadwal penyaluran:'),
const SizedBox(height: 8),
@ -474,9 +475,8 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
onPressed: () {
if (selectedJadwalId != null) {
// Panggil metode konfirmasi di controller
controller.konfirmasiPermintaanPenjadwalan(
permintaan['id'] ?? '',
selectedJadwalId ?? '',
controller.approveJadwal(
permintaan.id ?? '',
);
Get.back();
@ -508,7 +508,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
}
// Dialog untuk menolak permintaan
void _showTolakDialog(Map<String, dynamic> permintaan) {
void _showTolakDialog(PenyaluranBantuanModel permintaan) {
final TextEditingController alasanController = TextEditingController();
Get.dialog(
@ -519,7 +519,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Anda akan menolak permintaan penjadwalan dari ${permintaan['nama'] ?? 'Penerima'}.'),
'Anda akan menolak permintaan penjadwalan dari ${permintaan.judul ?? 'Penerima'}.'),
const SizedBox(height: 16),
const Text('Alasan penolakan:'),
const SizedBox(height: 8),
@ -542,8 +542,8 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
onPressed: () {
if (alasanController.text.trim().isNotEmpty) {
// Panggil metode tolak di controller
controller.tolakPermintaanPenjadwalan(
permintaan['id'] ?? '',
controller.rejectJadwal(
permintaan.id ?? '',
alasanController.text.trim(),
);

View File

@ -4,7 +4,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa
import 'package:penyaluran_app/app/modules/petugas_desa/views/dashboard_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/penyaluran_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/notifikasi_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/inventaris_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/stok_bantuan_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/penitipan_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/pengaduan_view.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
@ -35,7 +35,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
case 3:
return const Text('Pengaduan');
case 4:
return const Text('Inventaris');
return const Text('Stok Bantuan');
default:
return const Text('Petugas Desa');
}
@ -94,7 +94,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
],
);
// Tombol tambah untuk jadwal dan inventaris
// Tombol tambah untuk jadwal dan stok bantuan
if (activeTab == 1) {
return Row(
mainAxisSize: MainAxisSize.min,
@ -115,9 +115,9 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
children: [
IconButton(
icon: const Icon(Icons.add),
tooltip: 'Tambah Inventaris',
tooltip: 'Tambah Stok Bantuan',
onPressed: () {
// Implementasi untuk menambah inventaris baru
// Implementasi untuk menambah stok bantuan baru
},
),
notificationButton,
@ -171,7 +171,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
case 3:
return const PengaduanView();
case 4:
return const InventarisView();
return const StokBantuanView();
default:
return const DashboardView();
}
@ -325,7 +325,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
}),
Obx(() => ListTile(
leading: const Icon(Icons.inventory_2_outlined),
title: const Text('Inventaris'),
title: const Text('Stok Bantuan'),
selected: controller.activeTabIndex.value == 4,
selectedColor: AppTheme.primaryColor,
onTap: () {
@ -390,6 +390,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
onTap: () {
// Navigasi ke halaman profil
Navigator.pop(context);
Get.toNamed('/profile');
},
),
ListTile(
@ -623,7 +624,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
const BottomNavigationBarItem(
icon: Icon(Icons.inventory_2_outlined),
activeIcon: Icon(Icons.inventory_2),
label: 'Inventaris',
label: 'Stok Bantuan',
),
],
);

View File

@ -0,0 +1,768 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/date_formatter.dart';
class StokBantuanView extends GetView<StokBantuanController> {
const StokBantuanView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: RefreshIndicator(
onRefresh: controller.refreshData,
child: Obx(() => controller.isLoading.value
? const Center(child: CircularProgressIndicator())
: _buildContent(context)),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Tampilkan dialog tambah stok bantuan
_showAddStokDialog(context);
},
backgroundColor: AppTheme.primaryColor,
child: const Icon(Icons.add),
),
);
}
Widget _buildContent(BuildContext context) {
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Ringkasan stok bantuan
_buildStokBantuanSummary(context),
const SizedBox(height: 24),
// Filter dan pencarian
_buildFilterSearch(context),
const SizedBox(height: 20),
// Daftar stok bantuan
_buildStokBantuanList(context),
],
),
),
);
}
Widget _buildStokBantuanSummary(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ringkasan Stok Bantuan',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.inventory_2_outlined,
title: 'Total Stok',
value: DateFormatter.formatNumber(controller.totalStok.value),
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.input,
title: 'Masuk',
value: DateFormatter.formatNumber(controller.stokMasuk.value),
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.output,
title: 'Keluar',
value:
DateFormatter.formatNumber(controller.stokKeluar.value),
),
),
],
),
],
),
);
}
Widget _buildSummaryItem(
BuildContext context, {
required IconData icon,
required String title,
required String value,
}) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Colors.white,
size: 24,
),
),
const SizedBox(height: 8),
Text(
value,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white,
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildFilterSearch(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(
controller: controller.searchController,
decoration: InputDecoration(
hintText: 'Cari bantuan...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(vertical: 0),
),
),
),
const SizedBox(width: 12),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: IconButton(
onPressed: controller.refreshData,
icon: const Icon(Icons.refresh),
tooltip: 'Refresh',
),
),
],
);
}
Widget _buildStokBantuanList(BuildContext context) {
return Obx(() {
final filteredList = controller.getFilteredStokBantuan();
if (filteredList.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
const Icon(Icons.inventory_2_outlined,
size: 80, color: Colors.grey),
const SizedBox(height: 16),
Text(
controller.searchQuery.isEmpty
? 'Belum ada data stok bantuan'
: 'Tidak ada stok bantuan yang sesuai dengan pencarian',
style: const TextStyle(color: Colors.grey),
textAlign: TextAlign.center,
),
],
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Daftar Stok Bantuan',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'${filteredList.length} item',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
),
const SizedBox(height: 12),
...filteredList.map((item) => _buildStokBantuanItem(context, item)),
],
);
});
}
Widget _buildStokBantuanItem(BuildContext context, StokBantuanModel item) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(26),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item.nama ?? 'Tanpa Nama',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
item.status ?? 'TERSEDIA',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
),
),
),
],
),
if (item.deskripsi != null && item.deskripsi!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
item.deskripsi!,
style: Theme.of(context).textTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.inventory,
label: 'Jumlah',
value:
'${DateFormatter.formatNumber(item.jumlah)} ${item.satuan ?? ''}',
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.calendar_today,
label: 'Tanggal Masuk',
value: DateFormatter.formatDate(item.tanggalMasuk),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.timelapse,
label: 'Kadaluarsa',
value: DateFormatter.formatDate(item.tanggalKadaluarsa),
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.access_time,
label: 'Terakhir Diperbarui',
value: DateFormatter.formatDate(item.updatedAt),
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () {
// Tampilkan dialog edit stok bantuan
_showEditStokDialog(context, item);
},
icon: const Icon(Icons.edit_outlined, size: 18),
label: const Text('Edit'),
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
TextButton.icon(
onPressed: () {
// Tampilkan dialog konfirmasi hapus
_showDeleteConfirmation(context, item);
},
icon: const Icon(Icons.delete_outline, size: 18),
label: const Text('Hapus'),
style: TextButton.styleFrom(
foregroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
],
),
],
),
),
);
}
Widget _buildItemDetail(
BuildContext context, {
required IconData icon,
required String label,
required String value,
}) {
return Row(
children: [
Icon(
icon,
size: 16,
color: Colors.grey,
),
const SizedBox(width: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey,
),
),
Text(
value,
style: Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
);
}
void _showAddStokDialog(BuildContext context) {
final formKey = GlobalKey<FormState>();
final namaController = TextEditingController();
final jumlahController = TextEditingController();
final satuanController = TextEditingController();
final deskripsiController = TextEditingController();
DateTime? tanggalMasuk = DateTime.now();
DateTime? tanggalKadaluarsa;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Tambah Stok Bantuan'),
content: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: namaController,
decoration: const InputDecoration(
labelText: 'Nama Bantuan',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nama bantuan tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
controller: jumlahController,
decoration: const InputDecoration(
labelText: 'Jumlah',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Jumlah tidak boleh kosong';
}
if (double.tryParse(value) == null) {
return 'Jumlah harus berupa angka';
}
return null;
},
),
),
const SizedBox(width: 8),
Expanded(
flex: 1,
child: TextFormField(
controller: satuanController,
decoration: const InputDecoration(
labelText: 'Satuan',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Satuan tidak boleh kosong';
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: deskripsiController,
decoration: const InputDecoration(
labelText: 'Deskripsi',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 16),
InkWell(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: tanggalMasuk ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
tanggalMasuk = picked;
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Masuk',
border: OutlineInputBorder(),
),
child: Text(
DateFormatter.formatDate(tanggalMasuk),
),
),
),
const SizedBox(height: 16),
InkWell(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: tanggalKadaluarsa ??
DateTime.now().add(const Duration(days: 365)),
firstDate: DateTime.now(),
lastDate: DateTime(2030),
);
if (picked != null) {
tanggalKadaluarsa = picked;
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Kadaluarsa',
border: OutlineInputBorder(),
),
child: Text(
DateFormatter.formatDate(tanggalKadaluarsa),
),
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
final stok = StokBantuanModel(
nama: namaController.text,
jumlah: double.parse(jumlahController.text),
satuan: satuanController.text,
deskripsi: deskripsiController.text,
tanggalMasuk: tanggalMasuk,
tanggalKadaluarsa: tanggalKadaluarsa,
status: 'TERSEDIA',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
controller.addStok(stok);
Navigator.pop(context);
}
},
child: const Text('Simpan'),
),
],
),
);
}
void _showEditStokDialog(BuildContext context, StokBantuanModel stok) {
final formKey = GlobalKey<FormState>();
final namaController = TextEditingController(text: stok.nama);
final jumlahController =
TextEditingController(text: stok.jumlah?.toString());
final satuanController = TextEditingController(text: stok.satuan);
final deskripsiController = TextEditingController(text: stok.deskripsi);
DateTime? tanggalMasuk = stok.tanggalMasuk;
DateTime? tanggalKadaluarsa = stok.tanggalKadaluarsa;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Edit Stok Bantuan'),
content: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: namaController,
decoration: const InputDecoration(
labelText: 'Nama Bantuan',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nama bantuan tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
controller: jumlahController,
decoration: const InputDecoration(
labelText: 'Jumlah',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Jumlah tidak boleh kosong';
}
if (double.tryParse(value) == null) {
return 'Jumlah harus berupa angka';
}
return null;
},
),
),
const SizedBox(width: 8),
Expanded(
flex: 1,
child: TextFormField(
controller: satuanController,
decoration: const InputDecoration(
labelText: 'Satuan',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Satuan tidak boleh kosong';
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: deskripsiController,
decoration: const InputDecoration(
labelText: 'Deskripsi',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 16),
InkWell(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: tanggalMasuk ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
tanggalMasuk = picked;
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Masuk',
border: OutlineInputBorder(),
),
child: Text(
DateFormatter.formatDate(tanggalMasuk),
),
),
),
const SizedBox(height: 16),
InkWell(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: tanggalKadaluarsa ??
DateTime.now().add(const Duration(days: 365)),
firstDate: DateTime.now(),
lastDate: DateTime(2030),
);
if (picked != null) {
tanggalKadaluarsa = picked;
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Kadaluarsa',
border: OutlineInputBorder(),
),
child: Text(
DateFormatter.formatDate(tanggalKadaluarsa),
),
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
final updatedStok = StokBantuanModel(
id: stok.id,
nama: namaController.text,
jumlah: double.parse(jumlahController.text),
satuan: satuanController.text,
deskripsi: deskripsiController.text,
tanggalMasuk: tanggalMasuk,
tanggalKadaluarsa: tanggalKadaluarsa,
status: stok.status,
createdAt: stok.createdAt,
updatedAt: DateTime.now(),
);
controller.updateStok(updatedStok);
Navigator.pop(context);
}
},
child: const Text('Simpan'),
),
],
),
);
}
void _showDeleteConfirmation(BuildContext context, StokBantuanModel stok) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Konfirmasi Hapus'),
content: Text(
'Apakah Anda yakin ingin menghapus stok bantuan "${stok.nama}"?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
controller.deleteStok(stok.id ?? '');
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Hapus'),
),
],
),
);
}
}