import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:cashumit/models/receipt_item.dart'; import 'package:cashumit/utils/currency_format.dart'; /// Data yang dibutuhkan untuk merender invoice. class InvoiceData { final String invoiceNumber; final String storeName; final String storeAddress; final String adminName; final String adminPhone; final Uint8List? logoBytes; final Uint8List? qrisBytes; /// Gambar yang sudah didecode agar bisa dirender sinkron saat offscreen /// render (Image.memory bersifat async sehingga bisa tampil kosong). final ui.Image? logoImage; final ui.Image? qrisImage; final String customerName; // DITAGIH KEPADA final DateTime date; final List items; final double total; final double tipAmount; final String disclaimer; final String thankYouText; final String pantunText; InvoiceData({ required this.invoiceNumber, required this.storeName, required this.storeAddress, required this.adminName, required this.adminPhone, this.logoBytes, this.qrisBytes, this.logoImage, this.qrisImage, required this.customerName, required this.date, required this.items, required this.total, this.tipAmount = 0.0, this.disclaimer = '', this.thankYouText = '', this.pantunText = '', }); double get subtotal => items.fold(0.0, (s, i) => s + i.total); double get grandTotal => subtotal + tipAmount; } /// Widget invoice sesuai contoh (lebar tetap, background putih). class InvoiceImageWidget extends StatelessWidget { final InvoiceData data; const InvoiceImageWidget({super.key, required this.data}); @override Widget build(BuildContext context) { const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des' ]; final d = data.date; final dateStr = '${d.day} ${months[d.month - 1]} ${d.year}, ${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}'; final rupiah = (num v) => CurrencyFormat.formatRupiah(v); return Container( width: 420, padding: const EdgeInsets.all(20), color: Colors.white, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ // Header Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ if (data.logoImage != null) ClipRRect( borderRadius: BorderRadius.circular(12), child: RawImage( image: data.logoImage, width: 72, height: 72, fit: BoxFit.cover, ), ) else if (data.logoBytes != null) ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.memory( data.logoBytes!, width: 72, height: 72, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _logoFallback(data.storeName), ), ) else _logoFallback(data.storeName), const SizedBox(height: 10), _storeHeader(data), ], ), ), const SizedBox(width: 12), const Text('INVOICE', textAlign: TextAlign.right, style: TextStyle( fontSize: 26, fontWeight: FontWeight.w800, color: Colors.black)), ], ), const SizedBox(height: 16), // Kartu info Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFFF2F2F2), borderRadius: BorderRadius.circular(12), ), child: Column( children: [ _infoRow('DITAGIH KEPADA', data.customerName), const SizedBox(height: 8), _infoRow('NO. FAKTUR', data.invoiceNumber), const SizedBox(height: 8), _infoRow('DITERBITKAN', dateStr), ], ), ), const SizedBox(height: 16), // Tabel DefaultTextStyle( style: const TextStyle(color: Colors.black, fontSize: 12), child: Table( columnWidths: const { 0: FlexColumnWidth(2.6), 1: FlexColumnWidth(1.2), 2: FlexColumnWidth(1.0), 3: FlexColumnWidth(1.4), }, children: [ TableRow( decoration: const BoxDecoration( border: Border(bottom: BorderSide(color: Colors.black12)), ), children: [ _th('BARANG'), _th('HARGA', align: TextAlign.center), _th('KUANTITAS', align: TextAlign.center), _th('JUMLAH', align: TextAlign.right), ], ), ...data.items.map((it) => TableRow( children: [ _td(it.description), _td(rupiah(it.price), align: TextAlign.center), _td(_fmtQty(it.quantity), align: TextAlign.center), _td(rupiah(it.total), align: TextAlign.right), ], )), ], ), ), const SizedBox(height: 12), // Ringkasan _summaryRow('Subtotal', rupiah(data.subtotal)), if (data.tipAmount > 0) _summaryRow('Tip', rupiah(data.tipAmount)), _summaryRow('Total', rupiah(data.grandTotal)), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: const Color(0xFFF2F2F2), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ const Expanded( child: Text('Jumlah yang Harus Dibayar', style: TextStyle( fontWeight: FontWeight.w700, fontSize: 13, color: Colors.black)), ), const SizedBox(width: 8), Text(rupiah(data.grandTotal), style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13, color: Colors.black)), ], ), ), if (data.qrisImage != null || data.qrisBytes != null) ...[ const SizedBox(height: 16), const Text('Pembayaran via QRIS', style: TextStyle( fontWeight: FontWeight.w700, fontSize: 13, color: Colors.black)), const SizedBox(height: 8), Center( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( border: Border.all(color: Colors.black12), borderRadius: BorderRadius.circular(8), ), child: data.qrisImage != null ? RawImage( image: data.qrisImage, width: 180, height: 180, fit: BoxFit.contain, ) : Image.memory( data.qrisBytes!, width: 180, height: 180, fit: BoxFit.contain, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ), ), ), ], const SizedBox(height: 16), const Divider(), const SizedBox(height: 8), if (data.thankYouText.isNotEmpty) Text( data.thankYouText, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: Colors.black), textAlign: TextAlign.center, ), if (data.pantunText.isNotEmpty) ...[ const SizedBox(height: 4), ..._pantunLines(data.pantunText), ], if (data.disclaimer.isNotEmpty) ...[ const SizedBox(height: 8), Text( data.disclaimer, style: const TextStyle(fontSize: 10, color: Colors.black54), textAlign: TextAlign.center, ), ], if (data.storeName.isNotEmpty) ...[ const SizedBox(height: 4), Text(data.storeName, style: const TextStyle(fontSize: 10, color: Colors.black54), textAlign: TextAlign.center), ], ], ), ); } Widget _logoFallback(String name) { return Container( width: 72, height: 72, decoration: BoxDecoration( color: const Color(0xFFF2F2F2), borderRadius: BorderRadius.circular(12), ), alignment: Alignment.center, child: Text(name.isNotEmpty ? name[0].toUpperCase() : 'T', style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.black)), ); } Widget _storeHeader(InvoiceData data) { return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(data.storeName, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: Colors.black)), if (data.storeAddress.isNotEmpty) ...[ const SizedBox(height: 2), Text(data.storeAddress, style: const TextStyle(fontSize: 10, color: Colors.black87)), ], if (data.adminPhone.isNotEmpty) ...[ const SizedBox(height: 2), Text('Telp: ${data.adminPhone}', style: const TextStyle(fontSize: 10, color: Colors.black87)), ], if (data.adminName.isNotEmpty) ...[ const SizedBox(height: 2), Text('Admin: ${data.adminName}', style: const TextStyle(fontSize: 10, color: Colors.black87)), ], ], ); } static List _pantunLines(String text) { return text .split(RegExp(r'\\n|\n')) .map((line) => line.trim()) .where((line) => line.isNotEmpty) .map((line) => Text( line, style: const TextStyle(fontSize: 10, color: Colors.black87), textAlign: TextAlign.center, )) .toList(); } static Widget _infoRow(String label, String value) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 120, child: Text(label, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w700, color: Colors.black54)), ), Expanded( child: Text(value, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.black)), ), ], ); } static Widget _th(String t, {TextAlign align = TextAlign.left}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Text(t, textAlign: align, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: Colors.black54)), ); } static Widget _td(String t, {TextAlign align = TextAlign.left}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Text(t, textAlign: align, style: const TextStyle(fontSize: 12, color: Colors.black)), ); } static Widget _summaryRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: const TextStyle(fontSize: 12, color: Colors.black)), Text(value, style: const TextStyle(fontSize: 12, color: Colors.black)), ], ), ); } static String _fmtQty(double q) { if (q == q.truncateToDouble()) return q.toInt().toString(); return q.toStringAsFixed(2); } }