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 createState() => _HomeViewState(); } class _HomeViewState extends State 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 _rotateAnim; Future _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(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 _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(); // 🔥 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.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)), ], ), ); }), ), ); }), ], ), ) ], ), ), ), ), ), ); } }