Perbarui struktur dan referensi file di dashboard_view.dart dan detail_donatur_view.dart. Tambahkan dokumentasi pada kelas DateTimeHelper dan perkenalan fungsi baru untuk format tanggal relatif serta nama hari dan bulan. Hapus widget yang tidak digunakan seperti detail_penitipan_dialog.dart, loading_indicator.dart, navigation_button.dart, statistic_card.dart, dan status_pill.dart untuk menyederhanakan kode.

This commit is contained in:
Khafidh Fuadi
2025-03-16 16:30:23 +07:00
parent 5814b19546
commit 078d74aad3
22 changed files with 1639 additions and 509 deletions

View File

@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:penyaluran_app/app/theme/app_colors.dart';
/// Tombol navigasi yang digunakan untuk navigasi dalam aplikasi
///
/// Tombol ini memiliki label dan ikon, dan dapat dikonfigurasi untuk
/// berbagai ukuran dan warna.
class NavigationButton extends StatelessWidget {
/// Label yang ditampilkan pada tombol
final String label;
/// Ikon yang ditampilkan di sebelah label (opsional)
final IconData? icon;
/// Widget ikon kustom yang ditampilkan di sebelah label (opsional)
final Widget? iconWidget;
/// Fungsi yang dipanggil ketika tombol ditekan
final VoidCallback onPressed;
/// Warna latar belakang tombol
final Color? backgroundColor;
/// Warna teks dan ikon
final Color? foregroundColor;
/// Ukuran teks
final double fontSize;
/// Ukuran ikon
final double iconSize;
/// Konstruktor untuk NavigationButton
///
/// Salah satu dari [icon] atau [iconWidget] harus disediakan.
const NavigationButton({
super.key,
required this.label,
this.icon,
this.iconWidget,
required this.onPressed,
this.backgroundColor,
this.foregroundColor,
this.fontSize = 10,
this.iconSize = 12,
}) : assert(icon != null || iconWidget != null,
'Either icon or iconWidget must be provided');
@override
Widget build(BuildContext context) {
final Color bgColor = backgroundColor ?? AppColors.primary;
final Color fgColor = foregroundColor ?? const Color(0xFFAFF8FF);
return ConstrainedBox(
constraints: const BoxConstraints(minWidth: 70),
child: GestureDetector(
onTap: onPressed,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
label,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w500,
color: fgColor,
),
),
const SizedBox(width: 4),
if (icon != null)
Icon(
icon,
size: iconSize,
color: fgColor,
)
else if (iconWidget != null)
SizedBox(
width: iconSize,
height: iconSize,
child: iconWidget,
),
],
),
),
),
);
}
}
/// Data class untuk tombol navigasi
///
/// Digunakan untuk menyimpan data tombol navigasi yang akan digunakan
/// di beberapa tempat.
class NavigationButtonData {
/// Label yang ditampilkan pada tombol
final String label;
/// Ikon yang ditampilkan di sebelah label (opsional)
final IconData? icon;
/// Widget ikon kustom yang ditampilkan di sebelah label (opsional)
final Widget? iconWidget;
/// Konstruktor untuk NavigationButtonData
///
/// Salah satu dari [icon] atau [iconWidget] harus disediakan.
const NavigationButtonData({
required this.label,
this.icon,
this.iconWidget,
}) : assert(icon != null || iconWidget != null,
'Either icon or iconWidget must be provided');
}

View File

@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:penyaluran_app/app/theme/app_colors.dart';
/// Tombol utama yang digunakan di seluruh aplikasi
///
/// Tombol ini memiliki warna latar belakang utama dan teks putih.
/// Dapat dikonfigurasi untuk berbagai ukuran dan dapat dinonaktifkan.
class PrimaryButton extends StatelessWidget {
/// Teks yang ditampilkan pada tombol
final String text;
/// Fungsi yang dipanggil ketika tombol ditekan
final VoidCallback? onPressed;
/// Ikon yang ditampilkan di sebelah kiri teks (opsional)
final IconData? icon;
/// Apakah tombol mengisi lebar penuh
final bool fullWidth;
/// Ukuran tombol (kecil, sedang, besar)
final ButtonSize size;
/// Apakah tombol sedang memuat
final bool isLoading;
const PrimaryButton({
super.key,
required this.text,
this.onPressed,
this.icon,
this.fullWidth = false,
this.size = ButtonSize.medium,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
// Tentukan padding berdasarkan ukuran
final EdgeInsetsGeometry padding = _getPadding();
// Tentukan ukuran teks berdasarkan ukuran tombol
final double fontSize = _getFontSize();
return SizedBox(
width: fullWidth ? double.infinity : null,
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
padding: padding,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 0,
),
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: fontSize + 2),
const SizedBox(width: 8),
],
Text(
text,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
/// Mendapatkan padding berdasarkan ukuran tombol
EdgeInsetsGeometry _getPadding() {
switch (size) {
case ButtonSize.small:
return const EdgeInsets.symmetric(horizontal: 12, vertical: 8);
case ButtonSize.medium:
return const EdgeInsets.symmetric(horizontal: 16, vertical: 12);
case ButtonSize.large:
return const EdgeInsets.symmetric(horizontal: 24, vertical: 16);
}
}
/// Mendapatkan ukuran font berdasarkan ukuran tombol
double _getFontSize() {
switch (size) {
case ButtonSize.small:
return 12;
case ButtonSize.medium:
return 14;
case ButtonSize.large:
return 16;
}
}
}
/// Enum untuk ukuran tombol
enum ButtonSize {
small,
medium,
large,
}

View File

@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:penyaluran_app/app/theme/app_colors.dart';
import 'package:penyaluran_app/app/widgets/buttons/primary_button.dart';
/// Tombol sekunder yang digunakan di seluruh aplikasi
///
/// Tombol ini memiliki warna latar belakang putih dengan border dan teks berwarna utama.
/// Dapat dikonfigurasi untuk berbagai ukuran dan dapat dinonaktifkan.
class SecondaryButton extends StatelessWidget {
/// Teks yang ditampilkan pada tombol
final String text;
/// Fungsi yang dipanggil ketika tombol ditekan
final VoidCallback? onPressed;
/// Ikon yang ditampilkan di sebelah kiri teks (opsional)
final IconData? icon;
/// Apakah tombol mengisi lebar penuh
final bool fullWidth;
/// Ukuran tombol (kecil, sedang, besar)
final ButtonSize size;
/// Apakah tombol sedang memuat
final bool isLoading;
const SecondaryButton({
super.key,
required this.text,
this.onPressed,
this.icon,
this.fullWidth = false,
this.size = ButtonSize.medium,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
// Tentukan padding berdasarkan ukuran
final EdgeInsetsGeometry padding = _getPadding();
// Tentukan ukuran teks berdasarkan ukuran tombol
final double fontSize = _getFontSize();
return SizedBox(
width: fullWidth ? double.infinity : null,
child: OutlinedButton(
onPressed: isLoading ? null : onPressed,
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
side: BorderSide(color: AppColors.primary),
padding: padding,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primary),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: fontSize + 2),
const SizedBox(width: 8),
],
Text(
text,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
/// Mendapatkan padding berdasarkan ukuran tombol
EdgeInsetsGeometry _getPadding() {
switch (size) {
case ButtonSize.small:
return const EdgeInsets.symmetric(horizontal: 12, vertical: 8);
case ButtonSize.medium:
return const EdgeInsets.symmetric(horizontal: 16, vertical: 12);
case ButtonSize.large:
return const EdgeInsets.symmetric(horizontal: 24, vertical: 16);
}
}
/// Mendapatkan ukuran font berdasarkan ukuran tombol
double _getFontSize() {
switch (size) {
case ButtonSize.small:
return 12;
case ButtonSize.medium:
return 14;
case ButtonSize.large:
return 16;
}
}
}

View File

@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
import 'package:penyaluran_app/app/theme/app_colors.dart';
/// Kartu informasi yang digunakan untuk menampilkan informasi
///
/// Kartu ini memiliki judul, deskripsi, dan ikon, dan dapat dikonfigurasi
/// untuk berbagai ukuran dan warna.
class InfoCard extends StatelessWidget {
/// Judul kartu
final String title;
/// Deskripsi atau konten kartu
final String description;
/// Ikon yang ditampilkan di kartu (opsional)
final IconData? icon;
/// Widget ikon kustom yang ditampilkan di kartu (opsional)
final Widget? iconWidget;
/// Warna latar belakang kartu
final Color? backgroundColor;
/// Warna judul
final Color? titleColor;
/// Warna deskripsi
final Color? descriptionColor;
/// Warna ikon
final Color? iconColor;
/// Fungsi yang dipanggil ketika kartu ditekan (opsional)
final VoidCallback? onTap;
/// Padding kartu
final EdgeInsetsGeometry padding;
/// Margin kartu
final EdgeInsetsGeometry margin;
/// Konstruktor untuk InfoCard
const InfoCard({
super.key,
required this.title,
required this.description,
this.icon,
this.iconWidget,
this.backgroundColor,
this.titleColor,
this.descriptionColor,
this.iconColor,
this.onTap,
this.padding = const EdgeInsets.all(16),
this.margin = const EdgeInsets.all(0),
}) : assert(
icon != null ||
iconWidget != null ||
(icon == null && iconWidget == null),
'Cannot provide both icon and iconWidget');
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
final Color bgColor = backgroundColor ?? Colors.white;
final Color titleTextColor = titleColor ?? AppColors.textPrimary;
final Color descTextColor = descriptionColor ?? AppColors.textSecondary;
final Color iconColorValue = iconColor ?? AppColors.primary;
return Card(
margin: margin,
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: padding,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (icon != null)
Padding(
padding: const EdgeInsets.only(right: 16),
child: Icon(
icon,
size: 24,
color: iconColorValue,
),
)
else if (iconWidget != null)
Padding(
padding: const EdgeInsets.only(right: 16),
child: SizedBox(
width: 24,
height: 24,
child: iconWidget,
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: textTheme.titleMedium?.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
color: titleTextColor,
),
),
const SizedBox(height: 4),
Text(
description,
style: textTheme.bodyMedium?.copyWith(
fontSize: 14,
color: descTextColor,
),
),
],
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
/// Kartu statistik yang digunakan untuk menampilkan data statistik
///
/// Kartu ini memiliki judul, jumlah, dan subtitle, dan dapat dikonfigurasi
/// untuk berbagai ukuran dan warna.
class StatisticCard extends StatelessWidget {
/// Judul kartu
final String title;
/// Jumlah atau nilai statistik
final String count;
/// Subtitle atau deskripsi tambahan
final String subtitle;
/// Tinggi kartu
final double height;
/// Gradient latar belakang kartu
final Gradient? gradient;
/// Warna teks
final Color textColor;
/// Ikon yang ditampilkan di kartu (opsional)
final IconData? icon;
/// Warna ikon
final Color? iconColor;
/// Konstruktor untuk StatisticCard
const StatisticCard({
super.key,
required this.title,
required this.count,
required this.subtitle,
this.height = 100,
this.gradient,
this.textColor = Colors.white,
this.icon,
this.iconColor,
});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
final Gradient backgroundGradient = gradient ?? AppTheme.primaryGradient;
final Color iconColorValue = iconColor ?? textColor;
return Container(
width: double.infinity,
height: height,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: backgroundGradient,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(26),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: textTheme.bodyMedium?.copyWith(
fontSize: 14,
color: textColor.withAlpha(204),
),
),
const SizedBox(height: 8),
Text(
count,
style: textTheme.headlineSmall?.copyWith(
fontSize: 24,
fontWeight: FontWeight.bold,
color: textColor,
),
),
const Spacer(),
Text(
subtitle,
style: textTheme.bodySmall?.copyWith(
fontSize: 12,
color: textColor,
),
),
],
),
),
if (icon != null)
Icon(
icon,
size: 40,
color: iconColorValue.withAlpha(51),
),
],
),
);
}
}

View File

@ -1,17 +1,39 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/theme/app_colors.dart';
/// AppBar kustom yang digunakan di seluruh aplikasi
///
/// AppBar ini dapat dikonfigurasi untuk berbagai tampilan dan fungsi.
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
/// Judul yang ditampilkan di AppBar
final String title;
/// Apakah menampilkan tombol kembali
final bool showBackButton;
/// Daftar aksi yang ditampilkan di sebelah kanan AppBar
final List<Widget>? actions;
/// Widget yang ditampilkan di sebelah kiri AppBar
final Widget? leading;
/// Apakah judul berada di tengah
final bool centerTitle;
/// Elevasi AppBar
final double elevation;
/// Warna latar belakang AppBar
final Color? backgroundColor;
/// Warna konten AppBar
final Color? foregroundColor;
/// Fungsi yang dipanggil ketika tombol kembali ditekan
final VoidCallback? onBackPressed;
/// Konstruktor untuk CustomAppBar
const CustomAppBar({
super.key,
required this.title,
@ -22,6 +44,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
this.elevation = 0,
this.backgroundColor,
this.foregroundColor,
this.onBackPressed,
});
@override
@ -37,18 +60,24 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
),
centerTitle: centerTitle,
elevation: elevation,
backgroundColor: backgroundColor ?? AppTheme.primaryColor,
backgroundColor: backgroundColor ?? AppColors.primary,
foregroundColor: foregroundColor ?? Colors.white,
leading: showBackButton
? IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Get.back(),
)
: leading,
leading: _buildLeading(),
actions: actions,
);
}
/// Membangun widget leading berdasarkan parameter
Widget? _buildLeading() {
if (showBackButton) {
return IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: onBackPressed ?? () => Get.back(),
);
}
return leading;
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

View File

@ -1,306 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
/// Dialog untuk menampilkan detail penitipan bantuan
///
/// Contoh penggunaan:
/// ```dart
/// // Di halaman lain
/// void showDetailPenitipan(BuildContext context, PenitipanBantuanModel item) {
/// // Dapatkan data yang diperlukan
/// final donaturNama = item.donatur?.nama ?? 'Donatur tidak ditemukan';
/// final kategoriNama = item.kategoriBantuan?.nama ?? 'Kategori tidak ditemukan';
/// final kategoriSatuan = item.kategoriBantuan?.satuan ?? '';
///
/// // Tampilkan dialog
/// DetailPenitipanDialog.show(
/// context: context,
/// item: item,
/// donaturNama: donaturNama,
/// kategoriNama: kategoriNama,
/// kategoriSatuan: kategoriSatuan,
/// getPetugasDesaNama: (String? id) => 'Nama Petugas', // Sesuaikan dengan cara mendapatkan nama petugas
/// showFullScreenImage: (String imageUrl) {
/// DetailPenitipanDialog.showFullScreenImage(context, imageUrl);
/// },
/// );
/// }
class DetailPenitipanDialog {
static void show({
required BuildContext context,
required PenitipanBantuanModel item,
required String donaturNama,
required String kategoriNama,
required String kategoriSatuan,
required String Function(String?) getPetugasDesaNama,
required Function(String) showFullScreenImage,
}) {
// Cek apakah penitipan berbentuk uang
final isUang = item.isUang ?? false;
Get.dialog(
AlertDialog(
title: const Text('Detail Penitipan'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailItem('Donatur', donaturNama),
_buildDetailItem('Status', item.status ?? 'Tidak diketahui'),
_buildDetailItem('Kategori Bantuan', kategoriNama),
_buildDetailItem(
'Jumlah',
isUang
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
: '${DateTimeHelper.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,
defaultValue: 'Tidak ada tanggal'),
),
if (item.tanggalVerifikasi != null)
_buildDetailItem(
'Tanggal Verifikasi',
DateTimeHelper.formatDateTime(item.tanggalVerifikasi),
),
if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null)
_buildDetailItem(
'Diverifikasi Oleh',
getPetugasDesaNama(item.petugasDesaId),
),
_buildDetailItem('Tanggal Dibuat',
DateTimeHelper.formatDateTime(item.createdAt)),
if (item.alasanPenolakan != null &&
item.alasanPenolakan!.isNotEmpty)
_buildDetailItem('Alasan Penolakan', item.alasanPenolakan!),
// Foto Bantuan
if (!isUang &&
item.fotoBantuan != null &&
item.fotoBantuan!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
const Text(
'Foto Bantuan:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: item.fotoBantuan!.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
showFullScreenImage(item.fotoBantuan![index]);
},
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
item.fotoBantuan![index],
height: 100,
width: 100,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 100,
width: 100,
color: Colors.grey.shade300,
child: const Icon(Icons.error),
);
},
),
),
),
);
},
),
),
],
),
// Bukti Transfer (untuk bantuan uang)
if (isUang &&
item.fotoBantuan != null &&
item.fotoBantuan!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
const Text(
'Bukti Transfer:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: item.fotoBantuan!.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
showFullScreenImage(item.fotoBantuan![index]);
},
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
item.fotoBantuan![index],
height: 100,
width: 100,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 100,
width: 100,
color: Colors.grey.shade300,
child: const Icon(Icons.error),
);
},
),
),
),
);
},
),
),
],
),
// Bukti Serah Terima
if (item.fotoBuktiSerahTerima != null &&
item.fotoBuktiSerahTerima!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
const Text(
'Bukti Serah Terima:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
GestureDetector(
onTap: () {
showFullScreenImage(item.fotoBuktiSerahTerima!);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
item.fotoBuktiSerahTerima!,
height: 200,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 200,
width: double.infinity,
color: Colors.grey.shade300,
child: const Icon(Icons.error),
);
},
),
),
),
],
),
],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Tutup'),
),
],
),
);
}
static Widget _buildDetailItem(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
Text(
value,
style: const TextStyle(fontSize: 14),
),
const Divider(),
],
),
);
}
static 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,
),
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/theme/app_colors.dart';
/// Dialog konfirmasi yang digunakan di seluruh aplikasi
///
/// Dialog ini dapat dikonfigurasi untuk berbagai jenis konfirmasi.
class ConfirmationDialog {
/// Menampilkan dialog konfirmasi
///
/// [title] adalah judul dialog
/// [message] adalah pesan yang ditampilkan di dialog
/// [confirmText] adalah teks tombol konfirmasi
/// [cancelText] adalah teks tombol batal
/// [onConfirm] adalah fungsi yang dipanggil ketika tombol konfirmasi ditekan
/// [onCancel] adalah fungsi yang dipanggil ketika tombol batal ditekan
/// [isDanger] menentukan apakah dialog bersifat berbahaya (merah)
static Future<bool?> show({
required String title,
required String message,
String confirmText = 'Ya',
String cancelText = 'Batal',
VoidCallback? onConfirm,
VoidCallback? onCancel,
bool isDanger = false,
}) async {
return await Get.dialog<bool>(
AlertDialog(
title: Text(
title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isDanger ? AppColors.error : AppColors.textPrimary,
),
),
content: Text(
message,
style: TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
),
),
actions: [
// Tombol batal
TextButton(
onPressed: () {
Get.back(result: false);
if (onCancel != null) onCancel();
},
child: Text(
cancelText,
style: TextStyle(
color: AppColors.textSecondary,
),
),
),
// Tombol konfirmasi
TextButton(
onPressed: () {
Get.back(result: true);
if (onConfirm != null) onConfirm();
},
child: Text(
confirmText,
style: TextStyle(
color: isDanger ? AppColors.error : AppColors.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
barrierDismissible: false,
);
}
/// Menampilkan dialog konfirmasi berbahaya
///
/// Dialog ini memiliki warna merah untuk menandakan tindakan berbahaya.
static Future<bool?> showDanger({
required String title,
required String message,
String confirmText = 'Hapus',
String cancelText = 'Batal',
VoidCallback? onConfirm,
VoidCallback? onCancel,
}) async {
return await show(
title: title,
message: message,
confirmText: confirmText,
cancelText: cancelText,
onConfirm: onConfirm,
onCancel: onCancel,
isDanger: true,
);
}
}

View File

@ -0,0 +1,215 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
import 'package:penyaluran_app/app/theme/app_colors.dart';
/// Dialog untuk menampilkan detail penitipan bantuan
///
/// Dialog ini menampilkan informasi lengkap tentang penitipan bantuan.
class DetailPenitipanDialog {
/// Menampilkan dialog detail penitipan
///
/// [context] adalah BuildContext
/// [item] adalah model penitipan bantuan
/// [donaturNama] adalah nama donatur
/// [kategoriNama] adalah nama kategori bantuan
/// [kategoriSatuan] adalah satuan kategori bantuan
/// [getPetugasDesaNama] adalah fungsi untuk mendapatkan nama petugas desa
/// [showFullScreenImage] adalah fungsi untuk menampilkan gambar layar penuh
static void show({
required BuildContext context,
required PenitipanBantuanModel item,
required String donaturNama,
required String kategoriNama,
required String kategoriSatuan,
required String Function(String?) getPetugasDesaNama,
required Function(String) showFullScreenImage,
}) {
// Cek apakah penitipan berbentuk uang
final isUang = item.isUang ?? false;
Get.dialog(
AlertDialog(
title: const Text('Detail Penitipan'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow('ID', item.id ?? '-'),
_buildInfoRow('Donatur', donaturNama),
_buildInfoRow('Kategori', kategoriNama),
_buildInfoRow(
'Jumlah',
isUang
? 'Rp ${item.jumlah?.toStringAsFixed(0) ?? '0'}'
: '${item.jumlah?.toString() ?? '0'} $kategoriSatuan',
),
_buildInfoRow(
'Tanggal Penitipan',
DateTimeHelper.formatDateTime(
item.tanggalPenitipan ?? item.createdAt),
),
_buildInfoRow(
'Status',
item.status ?? 'Belum diproses',
),
if (item.petugasDesaId != null)
_buildInfoRow(
'Petugas Desa',
getPetugasDesaNama(item.petugasDesaId),
),
if (item.tanggalVerifikasi != null)
_buildInfoRow(
'Tanggal Verifikasi',
DateTimeHelper.formatDateTime(item.tanggalVerifikasi),
),
if (item.deskripsi != null && item.deskripsi!.isNotEmpty)
_buildInfoRow('Deskripsi', item.deskripsi!),
// Gambar bukti penitipan
if (item.fotoBantuan != null && item.fotoBantuan!.isNotEmpty) ...[
const SizedBox(height: 16),
const Text(
'Bukti Penitipan',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 8),
GestureDetector(
onTap: () => showFullScreenImage(item.fotoBantuan!.first),
child: Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: NetworkImage(item.fotoBantuan!.first),
fit: BoxFit.cover,
),
),
),
),
],
// Bukti serah terima
if (item.fotoBuktiSerahTerima != null &&
item.fotoBuktiSerahTerima!.isNotEmpty) ...[
const SizedBox(height: 16),
const Text(
'Bukti Serah Terima',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 8),
GestureDetector(
onTap: () => showFullScreenImage(item.fotoBuktiSerahTerima!),
child: Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: NetworkImage(item.fotoBuktiSerahTerima!),
fit: BoxFit.cover,
),
),
),
),
],
],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'Tutup',
style: TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
/// Menampilkan gambar dalam layar penuh
static void showFullScreenImage(BuildContext context, String imageUrl) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
backgroundColor: Colors.black,
iconTheme: const IconThemeData(color: Colors.white),
),
body: Container(
color: Colors.black,
child: Center(
child: InteractiveViewer(
panEnabled: true,
boundaryMargin: const EdgeInsets.all(20),
minScale: 0.5,
maxScale: 4,
child: Image.network(
imageUrl,
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return const Center(
child: Text(
'Gagal memuat gambar',
style: TextStyle(color: Colors.white),
),
);
},
),
),
),
),
),
),
);
}
/// Membangun baris informasi
static Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(fontSize: 14),
),
],
),
);
}
}

View File

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:penyaluran_app/app/widgets/indicators/loading_indicator.dart';
/// Indikator loading yang menempati seluruh layar
///
/// Indikator ini memiliki latar belakang semi-transparan dan dapat
/// dikonfigurasi dengan pesan.
class FullScreenLoading extends StatelessWidget {
/// Pesan yang ditampilkan di bawah indikator loading (opsional)
final String? message;
/// Warna indikator loading
final Color? color;
/// Warna latar belakang
final Color backgroundColor;
/// Apakah dapat dibatalkan dengan mengetuk di luar
final bool dismissible;
/// Konstruktor untuk FullScreenLoading
const FullScreenLoading({
super.key,
this.message,
this.color,
this.backgroundColor = Colors.black54,
this.dismissible = false,
});
@override
Widget build(BuildContext context) {
return Container(
color: backgroundColor,
child: LoadingIndicator(
message: message,
color: color ?? Colors.white,
textColor: Colors.white,
),
);
}
/// Menampilkan indikator loading di seluruh layar
static void show(BuildContext context,
{String? message, bool dismissible = false}) {
showDialog(
context: context,
barrierDismissible: dismissible,
builder: (context) => FullScreenLoading(
message: message,
dismissible: dismissible,
),
);
}
/// Menyembunyikan indikator loading
static void hide(BuildContext context) {
Navigator.of(context, rootNavigator: true).pop();
}
}

View File

@ -1,16 +1,41 @@
import 'package:flutter/material.dart';
import 'package:penyaluran_app/app/theme/app_colors.dart';
/// Indikator loading yang digunakan untuk menampilkan status loading
///
/// Indikator ini dapat dikonfigurasi dengan pesan, warna, dan ukuran.
class LoadingIndicator extends StatelessWidget {
/// Pesan yang ditampilkan di bawah indikator loading (opsional)
final String? message;
/// Warna indikator loading
final Color? color;
/// Ukuran indikator loading
final double size;
/// Ketebalan garis indikator loading
final double strokeWidth;
/// Warna teks pesan
final Color? textColor;
/// Ukuran teks pesan
final double textSize;
/// Jarak antara indikator loading dan pesan
final double spacing;
/// Konstruktor untuk LoadingIndicator
const LoadingIndicator({
super.key,
this.message,
this.color,
this.size = 40.0,
this.strokeWidth = 3.0,
this.textColor,
this.textSize = 16.0,
this.spacing = 16.0,
});
@override
@ -27,16 +52,16 @@ class LoadingIndicator extends StatelessWidget {
valueColor: AlwaysStoppedAnimation<Color>(
color ?? AppColors.primary,
),
strokeWidth: 3.0,
strokeWidth: strokeWidth,
),
),
if (message != null) ...[
const SizedBox(height: 16),
SizedBox(height: spacing),
Text(
message!,
style: TextStyle(
fontSize: 16,
color: Colors.grey[700],
fontSize: textSize,
color: textColor ?? Colors.grey[700],
),
textAlign: TextAlign.center,
),

View File

@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
/// Indikator status berbentuk pill yang digunakan untuk menampilkan status
///
/// Indikator ini memiliki teks status dan warna latar belakang yang dapat
/// dikonfigurasi.
class StatusPill extends StatelessWidget {
/// Teks status yang ditampilkan
final String status;
/// Warna latar belakang pill
final Color? backgroundColor;
/// Gaya teks status
final TextStyle? textStyle;
/// Warna teks status
final Color? textColor;
/// Padding pill
final EdgeInsetsGeometry padding;
/// Radius border pill
final double borderRadius;
/// Konstruktor untuk StatusPill
const StatusPill({
super.key,
required this.status,
this.backgroundColor,
this.textStyle,
this.textColor,
this.padding = const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
this.borderRadius = 12,
});
/// Konstruktor factory untuk StatusPill dengan status "Terverifikasi"
factory StatusPill.verified({String status = 'Terverifikasi'}) {
return StatusPill(
status: status,
backgroundColor: AppTheme.verifiedColor,
textColor: Colors.white,
);
}
/// Konstruktor factory untuk StatusPill dengan status "Diproses"
factory StatusPill.processed({String status = 'Diproses'}) {
return StatusPill(
status: status,
backgroundColor: AppTheme.processedColor,
textColor: Colors.white,
);
}
/// Konstruktor factory untuk StatusPill dengan status "Ditolak"
factory StatusPill.rejected({String status = 'Ditolak'}) {
return StatusPill(
status: status,
backgroundColor: AppTheme.rejectedColor,
textColor: Colors.white,
);
}
/// Konstruktor factory untuk StatusPill dengan status "Dijadwalkan"
factory StatusPill.scheduled({String status = 'Dijadwalkan'}) {
return StatusPill(
status: status,
backgroundColor: AppTheme.scheduledColor,
textColor: Colors.white,
);
}
/// Konstruktor factory untuk StatusPill dengan status "Selesai"
factory StatusPill.completed({String status = 'Selesai'}) {
return StatusPill(
status: status,
backgroundColor: AppTheme.completedColor,
textColor: Colors.white,
);
}
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Container(
padding: padding,
decoration: BoxDecoration(
color: backgroundColor ?? AppTheme.verifiedColor,
borderRadius: BorderRadius.circular(borderRadius),
),
child: Text(
status,
style: textStyle ??
textTheme.bodySmall?.copyWith(
fontSize: 10,
color: textColor ?? Colors.white,
),
),
);
}
}

View File

@ -0,0 +1,164 @@
import 'package:flutter/material.dart';
import 'package:penyaluran_app/app/theme/app_colors.dart';
/// Item untuk dropdown
class DropdownItem<T> {
/// Nilai item
final T value;
/// Label yang ditampilkan
final String label;
/// Konstruktor untuk DropdownItem
const DropdownItem({
required this.value,
required this.label,
});
}
/// Input dropdown yang digunakan di seluruh aplikasi
///
/// Input ini dapat dikonfigurasi untuk berbagai jenis dropdown dan validasi.
class DropdownInput<T> extends StatelessWidget {
/// Label yang ditampilkan di atas input
final String label;
/// Hint yang ditampilkan di dalam input
final String? hint;
/// Daftar item dropdown
final List<DropdownItem<T>> items;
/// Nilai yang dipilih
final T? value;
/// Fungsi yang dipanggil ketika nilai dropdown berubah
final Function(T?)? onChanged;
/// Apakah input dinonaktifkan
final bool enabled;
/// Apakah input bersifat wajib
final bool required;
/// Pesan kesalahan yang ditampilkan di bawah input
final String? errorText;
/// Fungsi validasi input
final String? Function(T?)? validator;
/// Konstruktor untuk DropdownInput
const DropdownInput({
super.key,
required this.label,
this.hint,
required this.items,
this.value,
this.onChanged,
this.enabled = true,
this.required = false,
this.errorText,
this.validator,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label
Row(
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
),
),
if (required)
Text(
' *',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.error,
),
),
],
),
const SizedBox(height: 8),
// Dropdown
DropdownButtonFormField<T>(
value: value,
onChanged: enabled ? onChanged : null,
validator: validator,
isExpanded: true,
icon: const Icon(Icons.arrow_drop_down),
style: TextStyle(
fontSize: 14,
color: enabled ? AppColors.textPrimary : AppColors.textSecondary,
),
decoration: InputDecoration(
hintText: hint,
errorText: errorText,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: AppColors.divider,
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: AppColors.divider,
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: AppColors.primary,
width: 1.5,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: AppColors.error,
width: 1,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: AppColors.error,
width: 1.5,
),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: AppColors.disabled,
width: 1,
),
),
),
items: items.map((DropdownItem<T> item) {
return DropdownMenuItem<T>(
value: item.value,
child: Text(item.label),
);
}).toList(),
),
],
);
}
}

View File

@ -0,0 +1,193 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:penyaluran_app/app/theme/app_colors.dart';
/// Input teks yang digunakan di seluruh aplikasi
///
/// Input ini dapat dikonfigurasi untuk berbagai jenis input dan validasi.
class TextInput extends StatelessWidget {
/// Label yang ditampilkan di atas input
final String label;
/// Hint yang ditampilkan di dalam input
final String? hint;
/// Controller untuk input
final TextEditingController? controller;
/// Fungsi yang dipanggil ketika nilai input berubah
final Function(String)? onChanged;
/// Fungsi yang dipanggil ketika input selesai diedit
final Function(String)? onSubmitted;
/// Apakah input dinonaktifkan
final bool enabled;
/// Apakah input bersifat wajib
final bool required;
/// Apakah input bersifat rahasia (password)
final bool obscureText;
/// Pesan kesalahan yang ditampilkan di bawah input
final String? errorText;
/// Ikon yang ditampilkan di sebelah kanan input
final IconData? suffixIcon;
/// Fungsi yang dipanggil ketika ikon di sebelah kanan input ditekan
final VoidCallback? onSuffixIconPressed;
/// Jenis keyboard yang digunakan
final TextInputType keyboardType;
/// Daftar pemformatan input
final List<TextInputFormatter>? inputFormatters;
/// Jumlah baris input (untuk input multiline)
final int? maxLines;
/// Jumlah karakter maksimum
final int? maxLength;
/// Apakah input otomatis mendapatkan fokus
final bool autofocus;
/// Fokus node untuk input
final FocusNode? focusNode;
/// Fungsi validasi input
final String? Function(String?)? validator;
/// Konstruktor untuk TextInput
const TextInput({
super.key,
required this.label,
this.hint,
this.controller,
this.onChanged,
this.onSubmitted,
this.enabled = true,
this.required = false,
this.obscureText = false,
this.errorText,
this.suffixIcon,
this.onSuffixIconPressed,
this.keyboardType = TextInputType.text,
this.inputFormatters,
this.maxLines = 1,
this.maxLength,
this.autofocus = false,
this.focusNode,
this.validator,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label
Row(
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
),
),
if (required)
Text(
' *',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.error,
),
),
],
),
const SizedBox(height: 8),
// Input
TextFormField(
controller: controller,
onChanged: onChanged,
onFieldSubmitted: onSubmitted,
enabled: enabled,
obscureText: obscureText,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
maxLines: maxLines,
maxLength: maxLength,
autofocus: autofocus,
focusNode: focusNode,
validator: validator,
style: TextStyle(
fontSize: 14,
color: enabled ? AppColors.textPrimary : AppColors.textSecondary,
),
decoration: InputDecoration(
hintText: hint,
errorText: errorText,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: AppColors.divider,
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: AppColors.divider,
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: AppColors.primary,
width: 1.5,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: AppColors.error,
width: 1,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: AppColors.error,
width: 1.5,
),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: AppColors.disabled,
width: 1,
),
),
suffixIcon: suffixIcon != null
? IconButton(
icon: Icon(suffixIcon),
onPressed: onSuffixIconPressed,
)
: null,
),
),
],
);
}
}

View File

@ -1,79 +0,0 @@
import 'package:flutter/material.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class NavigationButton extends StatelessWidget {
final String label;
final IconData? icon;
final Widget? iconWidget;
final VoidCallback onPressed;
const NavigationButton({
super.key,
required this.label,
this.icon,
this.iconWidget,
required this.onPressed,
}) : assert(icon != null || iconWidget != null,
'Either icon or iconWidget must be provided');
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return ConstrainedBox(
constraints: BoxConstraints(minWidth: 70), // Set a minimum width
child: GestureDetector(
onTap: onPressed,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize:
MainAxisSize.min, // Important for preventing layout issues
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
label,
style: textTheme.bodySmall?.copyWith(
fontSize: 10,
fontWeight: FontWeight.w500,
color: Color(0xFFAFF8FF),
),
),
const SizedBox(width: 4),
if (icon != null)
Icon(
icon,
size: 12,
color: Color(0xFFAFF8FF),
)
else if (iconWidget != null)
SizedBox(
width: 12,
height: 12,
child: iconWidget,
),
],
),
),
),
);
}
}
// Data class for navigation button
class NavigationButtonData {
final String label;
final IconData? icon;
final Widget? iconWidget;
NavigationButtonData({
required this.label,
this.icon,
this.iconWidget,
}) : assert(icon != null || iconWidget != null,
'Either icon or iconWidget must be provided');
}

View File

@ -1,61 +0,0 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
class StatisticCard extends StatelessWidget {
final String title;
final String count;
final String subtitle;
final double height;
const StatisticCard({
super.key,
required this.title,
required this.count,
required this.subtitle,
required this.height,
});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Container(
width: double.infinity,
height: height,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: textTheme.bodyMedium?.copyWith(
fontSize: 14,
color: Colors.white.withAlpha(204), // 0.8 * 255 ≈ 204
),
),
const SizedBox(height: 4),
Text(
count,
style: textTheme.headlineSmall?.copyWith(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: textTheme.bodySmall?.copyWith(
fontSize: 12,
color: Colors.white,
),
),
],
),
);
}
}

View File

@ -1,35 +0,0 @@
import 'package:flutter/material.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class StatusPill extends StatelessWidget {
final String status;
final Color? backgroundColor;
final TextStyle? textStyle;
const StatusPill({
super.key,
required this.status,
this.backgroundColor,
this.textStyle,
});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
decoration: BoxDecoration(
color: backgroundColor ?? AppTheme.verifiedColor,
borderRadius: BorderRadius.circular(12),
),
child: Text(
status,
style: textStyle ??
textTheme.bodySmall?.copyWith(
fontSize: 10,
),
),
);
}
}

View File

@ -0,0 +1,27 @@
// File ini berisi ekspor semua widget yang dapat digunakan kembali
// dalam aplikasi. Ini memudahkan pengimporan widget dengan
// menggunakan satu jalur impor.
// Buttons
export 'buttons/primary_button.dart';
export 'buttons/secondary_button.dart';
export 'buttons/navigation_button.dart';
// Cards
export 'cards/statistic_card.dart';
export 'cards/info_card.dart';
// Dialogs
export 'dialogs/detail_penitipan_dialog.dart';
export 'dialogs/confirmation_dialog.dart';
// Indicators
export 'indicators/loading_indicator.dart';
export 'indicators/status_pill.dart';
// Inputs
export 'inputs/text_input.dart';
export 'inputs/dropdown_input.dart';
// App Bar
export 'custom_app_bar.dart';