E41220983_MuhamadSugengCahy.../praresi/lib/presentation/views/riwayat_view.dart

752 lines
26 KiB
Dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:get/get.dart';
import 'package:praresi/presentation/routes/app_routes.dart';
import 'package:praresi/utils/pdf_export_daily_a4.dart';
import 'package:praresi/utils/pdf_export_daily_a4_multi.dart';
import 'package:praresi/utils/pdf_export_daily_a5.dart';
import 'package:praresi/utils/pdf_export_daily_a5_multi.dart';
import 'package:praresi/utils/pdf_export_daily_a6.dart';
import '../controllers/riwayat_controller.dart';
class RiwayatView extends StatefulWidget {
const RiwayatView({super.key});
@override
State<RiwayatView> createState() => _RiwayatViewState();
}
class _RiwayatViewState extends State<RiwayatView> {
DateTime? startDate;
DateTime? endDate;
final riwayatC = Get.put(RiwayatController(), permanent: true);
@override
void initState() {
super.initState();
final now = DateTime.now();
startDate = DateTime(now.year, now.month, 1);
endDate = DateTime(now.year, now.month + 1, 0, 23, 59, 59);
riwayatC.fetchResiData(); // 🔹 ambil data default (1 bulan terakhir)
}
Future<void> _selectDate(bool isStart) async {
final now = DateTime.now();
final picked = await showDatePicker(
context: context,
initialDate: isStart
? (startDate ?? DateTime(now.year, now.month, 1))
: (endDate ?? DateTime(now.year, now.month + 1, 0)),
firstDate: DateTime(2023),
lastDate: DateTime(2100),
builder: (context, child) => Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.light(
primary: Color(0xFF1976D2),
onPrimary: Colors.white,
onSurface: Colors.black,
),
),
child: child!,
),
);
if (picked != null) {
setState(() {
if (isStart) {
startDate = DateTime(picked.year, picked.month, picked.day);
} else {
// ⏰ tambahkan jam agar mencakup full hari terakhir
endDate = DateTime(picked.year, picked.month, picked.day, 23, 59, 59);
}
});
// 🔹 Kalau dua-duanya sudah terisi, ambil data
if (startDate != null && endDate != null) {
riwayatC.fetchResiData(start: startDate, end: endDate);
}
}
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Scaffold(
body: Container(
width: size.width,
height: size.height,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF1976D2), Color(0xFFE3F2FD)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: Column(
children: [
// 🔹 Card filter tanggal
Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Expanded(
child: _dateInputCard(
label: "Mulai dari",
date: startDate,
onTap: () => _selectDate(true),
),
),
const SizedBox(width: 12),
Expanded(
child: _dateInputCard(
label: "Hingga",
date: endDate,
onTap: () => _selectDate(false),
),
),
],
),
),
// 🔹 Garis pembatas putih dari kiri ke kanan
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Divider(
color: Colors.white, // warna garis
thickness: 1, // ketebalan garis
),
),
// 🔹 Daftar data dari Firestore
Expanded(
child: Obx(() {
if (riwayatC.isLoading.value) {
return const Center(
child:
CircularProgressIndicator(color: Color(0xFF1976D2)),
);
}
if (riwayatC.dailySummary.isEmpty) {
return const Center(
child: Text(
"Belum ada data resi",
style: TextStyle(color: Colors.black54, fontSize: 16),
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 10),
child: Column(
children: riwayatC.dailySummary.map((data) {
return Padding(
padding: const EdgeInsets.only(bottom: 15),
child: _dailySummaryCard(
day: data['day'],
date: data['date'],
total: data['total'],
cod: data['cod'],
nonCod: data['nonCod'],
income: data['income'],
),
);
}).toList(),
),
);
}),
),
],
),
),
),
);
}
// 🔹 Widget input tanggal
Widget _dateInputCard({
required String label,
required DateTime? date,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
fontWeight: FontWeight.w500,
)),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
date == null
? "Pilih tanggal"
: DateFormat('dd MMM yyyy', 'id_ID').format(date),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const Icon(Icons.calendar_today,
color: Color(0xFF1976D2), size: 18),
],
),
],
),
),
);
}
// 🔹 Widget tampilan per hari (sesuai desain kamu)
Widget _dailySummaryCard({
required String day,
required String date,
required int total,
required int cod,
required int nonCod,
required int income,
}) {
return GestureDetector(
onTap: () async {
final controller = Get.find<RiwayatController>();
try {
// ubah format tanggal "21 Oktober 2025" ke DateTime
final rawDate = DateFormat('d MMMM yyyy', 'id_ID').parse(date);
// tampilkan snackbar loading sementara
Get.snackbar(
'Memuat',
'Mengambil data detail...',
showProgressIndicator: true,
isDismissible: false,
snackPosition: SnackPosition.BOTTOM,
);
// ambil data dulu
await controller.fetchResiByDate(rawDate);
// tutup snackbar
Get.closeAllSnackbars();
// navigasi ke halaman detail (pakai named route)
Get.toNamed(
AppRoutes.detailRiwayat,
arguments: {
'day': day,
'date': date,
},
);
} catch (e) {
Get.closeAllSnackbars();
Get.snackbar('Error', 'Gagal membuka detail: $e');
}
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 8,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 🔹 Baris header: hari & tanggal + ikon print di kanan atas
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"$day, $date",
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
IconButton(
icon: const Icon(Icons.download, color: Color(0xFF1976D2)),
tooltip: "Unduh data hari ini",
onPressed: () {
_showPrintFormatDialog(context, day, date, riwayatC);
},
),
],
),
const Divider(),
const SizedBox(height: 5),
// 🔸 Total paket dan COD/NON-COD
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 2,
child: Container(
height: 90,
margin: const EdgeInsets.only(right: 10),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFF1976D2), width: 2),
borderRadius: BorderRadius.circular(14),
),
child: Center(
child: Text(
"Total \n$total",
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF1976D2),
),
),
),
),
),
Expanded(
flex: 2,
child: Column(
children: [
// 🔹 Card COD
InkWell(
onTap: () async {
final rawDate = DateFormat('d MMMM yyyy', 'id_ID').parse(date);
final pendapatan =
await riwayatC.getPendapatanHarian(rawDate);
final totalCod = pendapatan['cod'] ?? 0.0;
Get.defaultDialog(
title: "Pendapatan COD",
titleStyle: const TextStyle(
color: Color(0xFFFF9800),
fontWeight: FontWeight.bold,
fontSize: 18,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 16),
radius: 14,
content: Column(
children: [
Icon(Icons.local_shipping,
color: const Color(0xFFFF9800), size: 48),
const SizedBox(height: 12),
Text(
"Tanggal: ${DateFormat('d MMMM yyyy', 'id_ID').format(rawDate)}",
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 8),
Text(
"Rp ${NumberFormat('#,###', 'id_ID').format(totalCod)}",
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xFFFF9800),
),
),
],
),
confirm: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFF9800),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
),
onPressed: () => Get.back(),
child: const Text(
"Tutup",
style: TextStyle(color: Colors.white),
),
),
);
},
child: Container(
height: 40,
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFFF9800), width: 2),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
"COD: $cod",
style: const TextStyle(
color: Color(0xFFFF9800),
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
),
),
// 🔹 Card Non COD
InkWell(
onTap: () async {
final rawDate = DateFormat('d MMMM yyyy', 'id_ID').parse(date);
final pendapatan =
await riwayatC.getPendapatanHarian(rawDate);
final totalNonCod = pendapatan['nonCod'] ?? 0.0;
Get.defaultDialog(
title: "Pendapatan Non COD",
titleStyle: const TextStyle(
color: Color(0xFF4CAF50),
fontWeight: FontWeight.bold,
fontSize: 18,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 16),
radius: 14,
content: Column(
children: [
Icon(Icons.account_balance_wallet,
color: const Color(0xFF4CAF50), size: 48),
const SizedBox(height: 12),
Text(
"Tanggal: ${DateFormat('d MMMM yyyy', 'id_ID').format(rawDate)}",
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 8),
Text(
"Rp ${NumberFormat('#,###', 'id_ID').format(totalNonCod)}",
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xFF4CAF50),
),
),
],
),
confirm: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4CAF50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
),
onPressed: () => Get.back(),
child: const Text(
"Tutup",
style: TextStyle(color: Colors.white),
),
),
);
},
child: Container(
height: 40,
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFF4CAF50), width: 2),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
"Non COD: $nonCod",
style: const TextStyle(
color: Color(0xFF4CAF50),
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
),
),
],
),
),
],
),
const SizedBox(height: 10),
// 🔸 Pendapatan
Container(
width: double.infinity,
height: 40,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400, width: 1.2),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
"Rp ${NumberFormat('#,###', 'id_ID').format(income)}",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
),
],
),
),
);
}
void _showPrintFormatDialog(BuildContext context, String day, String date, dynamic controller,) {
Get.bottomSheet(
Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Wrap(
children: [
const Center(
child: Text(
"Pilih Format Unduh",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 20),
ListTile(
leading: const Icon(Icons.picture_as_pdf, color: Colors.blue),
title: const Text("Format A4"),
onTap: () async {
Get.back();
try {
// 🔹 Parse tanggal
final parsedDate =
DateFormat('d MMMM yyyy', 'id_ID').parse(date);
// 🔹 Ambil data harian
await controller.fetchResiByDate(parsedDate);
// 🔹 Export PDF (1 halaman = 1 data)
final path = await PdfExportDailyA4_onedata.exportDailyToA4_onedata(
controller.dailyDetail,
parsedDate,
);
Get.snackbar(
"Berhasil",
"PDF disimpan di: $path",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green.shade100,
);
} catch (e) {
Get.snackbar(
"Error",
"Gagal mencetak data: $e",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red.shade100,
);
}
},
),
ListTile(
leading: const Icon(Icons.picture_as_pdf, color: Colors.blue),
title: const Text("Format A5"),
onTap: () async {
Get.back();
try {
// 🔹 Parse tanggal "25 Oktober 2025" ke DateTime
final parsedDate =
DateFormat('d MMMM yyyy', 'id_ID').parse(date);
// 🔹 Ambil data harian
await controller.fetchResiByDate(parsedDate);
// 🔹 Cetak ke PDF format A4
final path = await PdfExportDailyA5_onedata.exportDailyToA5_onedata(
controller.dailyDetail,
parsedDate,
);
Get.snackbar(
"Berhasil",
"PDF disimpan di: $path",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green.shade100,
);
} catch (e) {
Get.snackbar(
"Error",
"Gagal mencetak data: $e",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red.shade100,
);
}
},
),
ListTile(
leading: const Icon(Icons.picture_as_pdf, color: Colors.blue),
title: const Text("Format A6"),
onTap: () async {
Get.back();
try {
// 🔹 Parse tanggal "25 Oktober 2025" ke DateTime
final parsedDate =
DateFormat('d MMMM yyyy', 'id_ID').parse(date);
// 🔹 Ambil data harian
await controller.fetchResiByDate(parsedDate);
// 🔹 Cetak ke PDF format A4
final path = await PdfExportDailyA6.exportDailyToA6(
controller.dailyDetail,
parsedDate,
);
Get.snackbar(
"Berhasil",
"PDF disimpan di: $path",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green.shade100,
);
} catch (e) {
Get.snackbar(
"Error",
"Gagal mengunduh data: $e",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red.shade100,
);
}
},
),
ListTile(
leading: const Icon(Icons.picture_as_pdf, color: Colors.blue),
title: const Text("Format A4 (Multi)"),
onTap: () async {
Get.back();
try {
// 🔹 Parse tanggal "25 Oktober 2025" ke DateTime
final parsedDate =
DateFormat('d MMMM yyyy', 'id_ID').parse(date);
// 🔹 Ambil data harian
await controller.fetchResiByDate(parsedDate);
// 🔹 Cetak ke PDF format A4
final path = await PdfExportDailyA4.exportDailyToA4(
controller.dailyDetail,
parsedDate,
);
Get.snackbar(
"Berhasil",
"PDF disimpan di: $path",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green.shade100,
);
} catch (e) {
Get.snackbar(
"Error",
"Gagal mencetak data: $e",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red.shade100,
);
}
},
),
ListTile(
leading: const Icon(Icons.picture_as_pdf, color: Colors.blue),
title: const Text("Format A5 (Multi)"),
onTap: () async {
Get.back();
try {
// 🔹 Parse tanggal "25 Oktober 2025" ke DateTime
final parsedDate =
DateFormat('d MMMM yyyy', 'id_ID').parse(date);
// 🔹 Ambil data harian
await controller.fetchResiByDate(parsedDate);
// 🔹 Cetak ke PDF format A4
final path = await PdfExportDailyA5.exportDailyToA5(
controller.dailyDetail,
parsedDate,
);
Get.snackbar(
"Berhasil",
"PDF disimpan di: $path",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green.shade100,
);
} catch (e) {
Get.snackbar(
"Error",
"Gagal mencetak data: $e",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red.shade100,
);
}
},
),
],
),
),
);
}
}