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:
@ -5,7 +5,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/components/progress_sect
|
|||||||
import 'package:penyaluran_app/app/modules/petugas_desa/components/schedule_card.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/components/schedule_card.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
|
||||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/statistic_card.dart';
|
import 'package:penyaluran_app/app/widgets/cards/statistic_card.dart';
|
||||||
|
|
||||||
class DashboardView extends GetView<PetugasDesaController> {
|
class DashboardView extends GetView<PetugasDesaController> {
|
||||||
const DashboardView({super.key});
|
const DashboardView({super.key});
|
||||||
|
@ -4,7 +4,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/donatur_cont
|
|||||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||||
import 'package:penyaluran_app/app/data/models/donatur_model.dart';
|
import 'package:penyaluran_app/app/data/models/donatur_model.dart';
|
||||||
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
|
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/detail_penitipan_dialog.dart';
|
import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart';
|
||||||
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
|
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
|
||||||
|
|
||||||
class DetailDonaturView extends GetView<DonaturController> {
|
class DetailDonaturView extends GetView<DonaturController> {
|
||||||
|
@ -5,7 +5,7 @@ import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
|
|||||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
|
||||||
import 'package:penyaluran_app/app/theme/app_theme.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/date_time_helper.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/detail_penitipan_dialog.dart';
|
import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
class PenitipanView extends GetView<PenitipanBantuanController> {
|
class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
/// Kelas pembantu untuk manipulasi tanggal dan waktu
|
||||||
|
///
|
||||||
|
/// Kelas ini berisi fungsi-fungsi untuk memformat dan memanipulasi
|
||||||
|
/// tanggal dan waktu.
|
||||||
class DateTimeHelper {
|
class DateTimeHelper {
|
||||||
/// Mengkonversi DateTime dari UTC ke timezone lokal
|
/// Mengkonversi DateTime dari UTC ke timezone lokal
|
||||||
static DateTime toLocalDateTime(DateTime utcDateTime) {
|
static DateTime toLocalDateTime(DateTime utcDateTime) {
|
||||||
@ -7,10 +11,17 @@ class DateTimeHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Format tanggal ke format Indonesia (dd MMM yyyy)
|
/// Format tanggal ke format Indonesia (dd MMM yyyy)
|
||||||
static String formatDate(DateTime? dateTime,
|
///
|
||||||
{String format = 'dd MMM yyyy',
|
/// [dateTime] adalah DateTime yang akan diformat
|
||||||
String locale = 'id_ID',
|
/// [format] adalah format yang digunakan
|
||||||
String defaultValue = 'Belum ditentukan'}) {
|
/// [locale] adalah locale yang digunakan
|
||||||
|
/// [defaultValue] adalah nilai default jika dateTime null
|
||||||
|
static String formatDate(
|
||||||
|
DateTime? dateTime, {
|
||||||
|
String format = 'dd MMM yyyy',
|
||||||
|
String locale = 'id_ID',
|
||||||
|
String defaultValue = 'Belum ditentukan',
|
||||||
|
}) {
|
||||||
if (dateTime == null) return defaultValue;
|
if (dateTime == null) return defaultValue;
|
||||||
|
|
||||||
// Pastikan tanggal dalam timezone lokal
|
// Pastikan tanggal dalam timezone lokal
|
||||||
@ -24,10 +35,17 @@ class DateTimeHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Format waktu ke format 24 jam (HH:mm)
|
/// Format waktu ke format 24 jam (HH:mm)
|
||||||
static String formatTime(DateTime? dateTime,
|
///
|
||||||
{String format = 'HH:mm',
|
/// [dateTime] adalah DateTime yang akan diformat
|
||||||
String locale = 'id_ID',
|
/// [format] adalah format yang digunakan
|
||||||
String defaultValue = 'Belum ditentukan'}) {
|
/// [locale] adalah locale yang digunakan
|
||||||
|
/// [defaultValue] adalah nilai default jika dateTime null
|
||||||
|
static String formatTime(
|
||||||
|
DateTime? dateTime, {
|
||||||
|
String format = 'HH:mm',
|
||||||
|
String locale = 'id_ID',
|
||||||
|
String defaultValue = 'Belum ditentukan',
|
||||||
|
}) {
|
||||||
if (dateTime == null) return defaultValue;
|
if (dateTime == null) return defaultValue;
|
||||||
|
|
||||||
// Pastikan waktu dalam timezone lokal
|
// Pastikan waktu dalam timezone lokal
|
||||||
@ -44,10 +62,17 @@ class DateTimeHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Format tanggal dan waktu (dd MMM yyyy HH:mm)
|
/// Format tanggal dan waktu (dd MMM yyyy HH:mm)
|
||||||
static String formatDateTime(DateTime? dateTime,
|
///
|
||||||
{String format = 'dd MMM yyyy HH:mm',
|
/// [dateTime] adalah DateTime yang akan diformat
|
||||||
String locale = 'id_ID',
|
/// [format] adalah format yang digunakan
|
||||||
String defaultValue = 'Belum ditentukan'}) {
|
/// [locale] adalah locale yang digunakan
|
||||||
|
/// [defaultValue] adalah nilai default jika dateTime null
|
||||||
|
static String formatDateTime(
|
||||||
|
DateTime? dateTime, {
|
||||||
|
String format = 'dd MMM yyyy HH:mm',
|
||||||
|
String locale = 'id_ID',
|
||||||
|
String defaultValue = 'Belum ditentukan',
|
||||||
|
}) {
|
||||||
if (dateTime == null) return defaultValue;
|
if (dateTime == null) return defaultValue;
|
||||||
|
|
||||||
// Pastikan tanggal dan waktu dalam timezone lokal
|
// Pastikan tanggal dan waktu dalam timezone lokal
|
||||||
@ -56,7 +81,83 @@ class DateTimeHelper {
|
|||||||
return DateFormat(format, locale).format(localDateTime);
|
return DateFormat(format, locale).format(localDateTime);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error formatting date time: $e');
|
print('Error formatting date time: $e');
|
||||||
return localDateTime.toString().split('.')[0]; // Fallback to basic format
|
return localDateTime.toString(); // Fallback to basic format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format tanggal relatif (hari ini, kemarin, dll)
|
||||||
|
///
|
||||||
|
/// [dateTime] adalah DateTime yang akan diformat
|
||||||
|
/// [locale] adalah locale yang digunakan
|
||||||
|
/// [defaultValue] adalah nilai default jika dateTime null
|
||||||
|
static String formatRelativeDate(
|
||||||
|
DateTime? dateTime, {
|
||||||
|
String locale = 'id_ID',
|
||||||
|
String defaultValue = 'Belum ditentukan',
|
||||||
|
}) {
|
||||||
|
if (dateTime == null) return defaultValue;
|
||||||
|
|
||||||
|
// Pastikan tanggal dalam timezone lokal
|
||||||
|
final localDateTime = toLocalDateTime(dateTime);
|
||||||
|
final now = DateTime.now();
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
final yesterday = today.subtract(const Duration(days: 1));
|
||||||
|
final tomorrow = today.add(const Duration(days: 1));
|
||||||
|
final aDate =
|
||||||
|
DateTime(localDateTime.year, localDateTime.month, localDateTime.day);
|
||||||
|
|
||||||
|
if (aDate == today) {
|
||||||
|
return 'Hari ini, ${formatTime(localDateTime)}';
|
||||||
|
} else if (aDate == yesterday) {
|
||||||
|
return 'Kemarin, ${formatTime(localDateTime)}';
|
||||||
|
} else if (aDate == tomorrow) {
|
||||||
|
return 'Besok, ${formatTime(localDateTime)}';
|
||||||
|
} else {
|
||||||
|
return formatDateTime(localDateTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mendapatkan nama hari dari DateTime
|
||||||
|
///
|
||||||
|
/// [dateTime] adalah DateTime yang akan diambil nama harinya
|
||||||
|
/// [locale] adalah locale yang digunakan
|
||||||
|
/// [defaultValue] adalah nilai default jika dateTime null
|
||||||
|
static String getDayName(
|
||||||
|
DateTime? dateTime, {
|
||||||
|
String locale = 'id_ID',
|
||||||
|
String defaultValue = 'Belum ditentukan',
|
||||||
|
}) {
|
||||||
|
if (dateTime == null) return defaultValue;
|
||||||
|
|
||||||
|
// Pastikan tanggal dalam timezone lokal
|
||||||
|
final localDateTime = toLocalDateTime(dateTime);
|
||||||
|
try {
|
||||||
|
return DateFormat('EEEE', locale).format(localDateTime);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error getting day name: $e');
|
||||||
|
return ''; // Fallback to empty string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mendapatkan nama bulan dari DateTime
|
||||||
|
///
|
||||||
|
/// [dateTime] adalah DateTime yang akan diambil nama bulannya
|
||||||
|
/// [locale] adalah locale yang digunakan
|
||||||
|
/// [defaultValue] adalah nilai default jika dateTime null
|
||||||
|
static String getMonthName(
|
||||||
|
DateTime? dateTime, {
|
||||||
|
String locale = 'id_ID',
|
||||||
|
String defaultValue = 'Belum ditentukan',
|
||||||
|
}) {
|
||||||
|
if (dateTime == null) return defaultValue;
|
||||||
|
|
||||||
|
// Pastikan tanggal dalam timezone lokal
|
||||||
|
final localDateTime = toLocalDateTime(dateTime);
|
||||||
|
try {
|
||||||
|
return DateFormat('MMMM', locale).format(localDateTime);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error getting month name: $e');
|
||||||
|
return ''; // Fallback to empty string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
120
lib/app/widgets/buttons/navigation_button.dart
Normal file
120
lib/app/widgets/buttons/navigation_button.dart
Normal 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');
|
||||||
|
}
|
118
lib/app/widgets/buttons/primary_button.dart
Normal file
118
lib/app/widgets/buttons/primary_button.dart
Normal 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,
|
||||||
|
}
|
111
lib/app/widgets/buttons/secondary_button.dart
Normal file
111
lib/app/widgets/buttons/secondary_button.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
135
lib/app/widgets/cards/info_card.dart
Normal file
135
lib/app/widgets/cards/info_card.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
111
lib/app/widgets/cards/statistic_card.dart
Normal file
111
lib/app/widgets/cards/statistic_card.dart
Normal 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,39 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.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 {
|
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
/// Judul yang ditampilkan di AppBar
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
|
/// Apakah menampilkan tombol kembali
|
||||||
final bool showBackButton;
|
final bool showBackButton;
|
||||||
|
|
||||||
|
/// Daftar aksi yang ditampilkan di sebelah kanan AppBar
|
||||||
final List<Widget>? actions;
|
final List<Widget>? actions;
|
||||||
|
|
||||||
|
/// Widget yang ditampilkan di sebelah kiri AppBar
|
||||||
final Widget? leading;
|
final Widget? leading;
|
||||||
|
|
||||||
|
/// Apakah judul berada di tengah
|
||||||
final bool centerTitle;
|
final bool centerTitle;
|
||||||
|
|
||||||
|
/// Elevasi AppBar
|
||||||
final double elevation;
|
final double elevation;
|
||||||
|
|
||||||
|
/// Warna latar belakang AppBar
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
|
|
||||||
|
/// Warna konten AppBar
|
||||||
final Color? foregroundColor;
|
final Color? foregroundColor;
|
||||||
|
|
||||||
|
/// Fungsi yang dipanggil ketika tombol kembali ditekan
|
||||||
|
final VoidCallback? onBackPressed;
|
||||||
|
|
||||||
|
/// Konstruktor untuk CustomAppBar
|
||||||
const CustomAppBar({
|
const CustomAppBar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
@ -22,6 +44,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
this.elevation = 0,
|
this.elevation = 0,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.foregroundColor,
|
this.foregroundColor,
|
||||||
|
this.onBackPressed,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -37,18 +60,24 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
centerTitle: centerTitle,
|
centerTitle: centerTitle,
|
||||||
elevation: elevation,
|
elevation: elevation,
|
||||||
backgroundColor: backgroundColor ?? AppTheme.primaryColor,
|
backgroundColor: backgroundColor ?? AppColors.primary,
|
||||||
foregroundColor: foregroundColor ?? Colors.white,
|
foregroundColor: foregroundColor ?? Colors.white,
|
||||||
leading: showBackButton
|
leading: _buildLeading(),
|
||||||
? IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
)
|
|
||||||
: leading,
|
|
||||||
actions: actions,
|
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
|
@override
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
100
lib/app/widgets/dialogs/confirmation_dialog.dart
Normal file
100
lib/app/widgets/dialogs/confirmation_dialog.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
215
lib/app/widgets/dialogs/detail_penitipan_dialog.dart
Normal file
215
lib/app/widgets/dialogs/detail_penitipan_dialog.dart
Normal 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
59
lib/app/widgets/indicators/full_screen_loading.dart
Normal file
59
lib/app/widgets/indicators/full_screen_loading.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,41 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:penyaluran_app/app/theme/app_colors.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 {
|
class LoadingIndicator extends StatelessWidget {
|
||||||
|
/// Pesan yang ditampilkan di bawah indikator loading (opsional)
|
||||||
final String? message;
|
final String? message;
|
||||||
|
|
||||||
|
/// Warna indikator loading
|
||||||
final Color? color;
|
final Color? color;
|
||||||
|
|
||||||
|
/// Ukuran indikator loading
|
||||||
final double size;
|
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({
|
const LoadingIndicator({
|
||||||
super.key,
|
super.key,
|
||||||
this.message,
|
this.message,
|
||||||
this.color,
|
this.color,
|
||||||
this.size = 40.0,
|
this.size = 40.0,
|
||||||
|
this.strokeWidth = 3.0,
|
||||||
|
this.textColor,
|
||||||
|
this.textSize = 16.0,
|
||||||
|
this.spacing = 16.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -27,16 +52,16 @@ class LoadingIndicator extends StatelessWidget {
|
|||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
color ?? AppColors.primary,
|
color ?? AppColors.primary,
|
||||||
),
|
),
|
||||||
strokeWidth: 3.0,
|
strokeWidth: strokeWidth,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (message != null) ...[
|
if (message != null) ...[
|
||||||
const SizedBox(height: 16),
|
SizedBox(height: spacing),
|
||||||
Text(
|
Text(
|
||||||
message!,
|
message!,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: textSize,
|
||||||
color: Colors.grey[700],
|
color: textColor ?? Colors.grey[700],
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
103
lib/app/widgets/indicators/status_pill.dart
Normal file
103
lib/app/widgets/indicators/status_pill.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
164
lib/app/widgets/inputs/dropdown_input.dart
Normal file
164
lib/app/widgets/inputs/dropdown_input.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
193
lib/app/widgets/inputs/text_input.dart
Normal file
193
lib/app/widgets/inputs/text_input.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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');
|
|
||||||
}
|
|
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
27
lib/app/widgets/widgets.dart
Normal file
27
lib/app/widgets/widgets.dart
Normal 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';
|
Reference in New Issue
Block a user