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

1046 lines
41 KiB
Dart

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import 'package:praresi/presentation/controllers/home_controller.dart';
import 'package:praresi/presentation/controllers/riwayat_controller.dart';
import 'package:praresi/services/bubble_services.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';
class HomeView extends StatefulWidget {
const HomeView({super.key});
@override
State<HomeView> createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> with SingleTickerProviderStateMixin {
bool isBubbleActive = false;
String lastName = 'Pengguna';
String greeting = '';
IconData timeIcon = Icons.wb_sunny_outlined;
final DashboardController c = Get.put(DashboardController());
final riwayatC = Get.put(RiwayatController(), permanent: true);
late AnimationController _controller;
late Animation<double> _rotateAnim;
Future<void> _refreshData() async {
// Matikan dulu listener lama biar tidak dobel
c.onClose();
// Jalankan ulang listener realtime
c.onInit();
// Bisa juga muat ulang user data
await _loadUserData();
// Delay kecil supaya animasi refresh terasa smooth
await Future.delayed(const Duration(seconds: 1));
}
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,
);
}
},
),
// 🔹 FORMAT A6
ListTile(
leading: const Icon(Icons.picture_as_pdf, color: Colors.blue),
title: const Text("Format A6"),
onTap: () async {
Get.back();
try {
final parsedDate = DateFormat('d MMMM yyyy', 'id_ID').parse(date);
await controller.fetchResiByDate(parsedDate);
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 mencetak data: $e",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red.shade100,
);
}
},
),
// 🔹 FORMAT A4
ListTile(
leading: const Icon(Icons.picture_as_pdf, color: Colors.blue),
title: const Text("Format A4 (Multi)"),
onTap: () async {
Get.back();
try {
final parsedDate = DateFormat('d MMMM yyyy', 'id_ID').parse(date);
await controller.fetchResiByDate(parsedDate);
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,
);
}
},
),
// 🔹 FORMAT A5
ListTile(
leading: const Icon(Icons.picture_as_pdf, color: Colors.blue),
title: const Text("Format A5 (Multi)"),
onTap: () async {
Get.back();
try {
final parsedDate = DateFormat('d MMMM yyyy', 'id_ID').parse(date);
await controller.fetchResiByDate(parsedDate);
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,
);
}
},
),
],
),
),
);
}
@override
void initState() {
super.initState();
_setGreeting();
_loadUserData();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_rotateAnim = Tween<double>(begin: 0, end: 2 * pi).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
void _salinFormat(BuildContext context) {
const formatText = '''
Penerima :
Alamat :
No. Wa :
Barang :
Total :
Pembayaran :
''';
Clipboard.setData(const ClipboardData(text: formatText));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Format pengiriman berhasil disalin!"),
duration: Duration(seconds: 2),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _setGreeting() {
final hour = DateTime.now().hour;
if (hour < 11) {
greeting = 'Selamat Pagi,';
timeIcon = Icons.wb_twilight;
} else if (hour < 15) {
greeting = 'Selamat Siang,';
timeIcon = Icons.wb_sunny_outlined;
} else if (hour < 18) {
greeting = 'Selamat Sore,';
timeIcon = Icons.wb_sunny_sharp;
} else {
greeting = 'Selamat Malam,';
timeIcon = Icons.bedtime_rounded;
}
}
Future<void> _loadUserData() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
try {
final doc = await FirebaseFirestore.instance.collection('users').doc(user.uid).get();
if (doc.exists) {
final name = (doc.data()?['name'] ?? '').toString().trim();
if (name.isNotEmpty) {
final parts = name.split(RegExp(r'\s+'));
setState(() {
lastName = parts.isNotEmpty ? parts.last : name;
});
}
}
} catch (_) {}
}
void _animateIcon() {
if (_controller.isAnimating) return;
_controller.forward(from: 0);
}
@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: RefreshIndicator(
color: const Color(0xFF1976D2),
onRefresh: _refreshData,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// === CARD SAPAAN ===
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.10),
blurRadius: 10,
offset: const Offset(0, 6),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
greeting,
style: const TextStyle(
fontSize: 16,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
lastName,
style: const TextStyle(
fontSize: 24,
color: Color(0xFF1976D2),
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'Buat dan cetak alamat pengiriman Anda dengan cepat dan mudah!',
textAlign: TextAlign.justify,
style: TextStyle(
fontSize: 14,
color: Colors.black54,
height: 1.3,
),
),
],
),
),
GestureDetector(
onTap: _animateIcon,
child: AnimatedBuilder(
animation: _rotateAnim,
builder: (context, child) {
return Transform.rotate(
angle: _rotateAnim.value,
child: Icon(
timeIcon,
color: const Color(0xFF1976D2),
size: 42,
),
);
},
),
),
],
),
),
const SizedBox(height: 16),
// === CARD CETAK DAN BUBBLE SEJAJAR ===
Row(
children: [
Expanded(
child: Container(
height: 85,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Center(
child: ElevatedButton.icon(
onPressed: () async {
final now = DateTime.now();
final day = DateFormat.EEEE('id_ID').format(now);
final date = DateFormat('d MMMM yyyy', 'id_ID').format(now);
final controller = Get.find<RiwayatController>();
// 🔥 Ambil data hari ini dulu
await controller.fetchResiByDate(now);
// 🔥 CEK DATA
if (controller.dailyDetail.isEmpty) {
Get.snackbar(
"Tidak Ada Data",
"Belum ada riwayat pengiriman hari ini.",
backgroundColor: Colors.orange.shade100,
colorText: Colors.black,
snackPosition: SnackPosition.BOTTOM,
);
return;
}
// ✅ Kalau ada data → lanjut
_showPrintFormatDialog(context, day, date, controller);
},
icon: const Icon(Icons.download),
label: const Text('Unduh Data Pengiriman'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: const Color(0xFF1976D2),
elevation: 0,
),
),
),
),
),
const SizedBox(width: 14),
Expanded(
child: Container(
height: 85,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Center(
child: ElevatedButton.icon(
onPressed: () => _salinFormat(context),
icon: const Icon(Icons.copy),
label: const Text('Salin Format'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: const Color(0xFF1976D2),
elevation: 0,
),
),
),
),
),
],
),
const SizedBox(height: 16),
// === CARD PENDAPATAN HARI INI ===
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.10),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Bagian kiri: teks
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pendapatan Hari Ini',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
SizedBox(height: 8),
Text(
"${c.formatRupiah(c.totalPendapatan.value)}",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1976D2),
),
),
],
),
// Bagian kanan: ikon uang
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFF1976D2), // warna biru border
width: 1, // ketebalan border
),
),
child: const Icon(
Icons.attach_money_outlined,
color: Color(0xFF1976D2),
size: 32,
),
)
],
),
),
const SizedBox(height: 16),
// === CARD PENGIRIMAN BESAR ===
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.10),
blurRadius: 10,
offset: const Offset(0, 6),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pengiriman',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Color(0xFF1976D2),
),
),
const SizedBox(height: 12),
Row(
children: [
// KIRI: TOTAL
Expanded(
flex: 2,
child: Container(
height: 150,
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFF1976D2), width: 2),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Total',
style: TextStyle(
color: Color(0xFF1976D2),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 8),
Text(
"${c.totalPaket.value}",
style: TextStyle(
color: Color(0xFF1976D2),
fontSize: 36,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
const SizedBox(width: 12),
// KANAN: COD & NON COD
Expanded(
flex: 2,
child: Column(
children: [
// ✅ Card COD
InkWell(
onTap: () {
showDialog(
context: context,
builder: (context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.15),
shape: BoxShape.circle,
),
padding: const EdgeInsets.all(16),
child: const Icon(
Icons.attach_money_rounded,
color: Colors.orange,
size: 48,
),
),
const SizedBox(height: 16),
const Text(
'Pendapatan COD Hari Ini',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Colors.black87,
),
),
const SizedBox(height: 12),
Text(
"${c.formatRupiah(c.totalPendapatanCOD.value)}",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: Colors.orange,
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text(
'Tutup',
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
],
),
),
);
},
);
},
child: Container(
height: 68,
decoration: BoxDecoration(
border: Border.all(color: Colors.orange, width: 2),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
"COD : ${c.totalPaketCOD.value}",
style: TextStyle(
color: Colors.orange,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
),
const SizedBox(height: 12),
// ✅ Card Non-COD
InkWell(
onTap: () {
showDialog(
context: context,
builder: (context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.15),
shape: BoxShape.circle,
),
padding: const EdgeInsets.all(16),
child: const Icon(
Icons.payments_rounded,
color: Colors.green,
size: 48,
),
),
const SizedBox(height: 16),
const Text(
'Pendapatan Non-COD Hari Ini',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Colors.black87,
),
),
const SizedBox(height: 12),
Text(
"${c.formatRupiah(c.totalPendapatanNonCOD.value)}",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: Colors.green,
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text(
'Tutup',
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
],
),
),
);
},
);
},
child: Container(
height: 68,
decoration: BoxDecoration(
border: Border.all(color: Colors.green, width: 2),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
"Non COD : ${c.totalPaketNonCOD.value}",
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
),
],
),
)
],
),
],
),
),
const SizedBox(height: 24),
// === CARD GRAFIK PENGIRIMAN ===
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.10),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Grafik Pengiriman Harian',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Colors.black87,
),
),
const SizedBox(height: 12),
// === Grafik Garis ===
SizedBox(
height: 200,
child: Obx(() {
if (c.grafikPengiriman.isEmpty) {
return const Center(child: Text("Belum ada data pengiriman"));
}
final now = DateTime.now();
final startDate = now.subtract(const Duration(days: 6));
final todayIndex = 6;
final spots = List<FlSpot>.from(c.grafikPengiriman)
..sort((a, b) => a.x.compareTo(b.x));
return LineChart(
LineChartData(
minX: 0,
maxX: 6,
minY: 0,
maxY: spots.map((e) => e.y).reduce((a, b) => a > b ? a : b) + 1,
gridData: const FlGridData(show: true, drawVerticalLine: false),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
if (value < 0 || value > 6) return const SizedBox();
final date = startDate.add(Duration(days: value.toInt()));
final dayLabel =
DateFormat('E', 'id_ID').format(date); // Sen, Sel, dll
final isToday = value.toInt() == todayIndex;
return Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(
dayLabel,
style: TextStyle(
fontSize: 12,
fontWeight:
isToday ? FontWeight.bold : FontWeight.normal,
color: isToday ? Colors.red : Colors.black,
),
),
);
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 32,
getTitlesWidget: (value, meta) {
if (value % 1 != 0) return const SizedBox();
return Text(
value.toInt().toString(),
style: const TextStyle(
fontSize: 10, color: Colors.black54),
);
},
),
),
),
borderData: FlBorderData(
show: true,
border: const Border(
top: BorderSide.none,
right: BorderSide.none,
left: BorderSide(color: Colors.black12),
bottom: BorderSide(color: Colors.black12),
),
),
lineBarsData: [
LineChartBarData(
isCurved: true,
color: const Color(0xFF1976D2),
barWidth: 3,
isStrokeCapRound: true,
spots: spots,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
const Color(0xFF1976D2).withOpacity(0.3),
Colors.transparent
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
final isToday = spot.x == todayIndex.toDouble();
return FlDotCirclePainter(
radius: isToday ? 5 : 3,
color: isToday ? Colors.red : Colors.blueAccent,
strokeWidth: 2,
strokeColor: Colors.white,
);
},
),
),
],
),
);
}),
),
const SizedBox(height: 16),
// === Card Daftar Total Pengiriman per Hari ===
Obx(() {
if (c.grafikPengiriman.isEmpty) return const SizedBox();
final now = DateTime.now();
final startDate = now.subtract(const Duration(days: 6));
return Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.black12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(7, (index) {
final date = startDate.add(Duration(days: index));
final dayLabel = DateFormat('EEEE', 'id_ID').format(date);
final value = c.grafikPengiriman
.firstWhereOrNull((e) => e.x == index.toDouble())
?.y
.toInt() ??
0;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(dayLabel, style: const TextStyle(fontSize: 13)),
Text("$value resi",
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 13)),
],
),
);
}),
),
);
}),
],
),
)
],
),
),
),
),
),
);
}
}