import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:intl/intl.dart'; import 'package:screenshot/screenshot.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import '../../inventory/providers/products_provider.dart'; import '../domain/invoice.dart'; import '../providers/invoices_provider.dart'; import '../providers/customers_provider.dart'; import '../../business/providers/business_provider.dart'; import 'widgets/receive_payment_sheet.dart'; class InvoiceDetailsScreen extends ConsumerStatefulWidget { final Invoice invoice; const InvoiceDetailsScreen({super.key, required this.invoice}); @override ConsumerState createState() => _InvoiceDetailsScreenState(); } class _InvoiceDetailsScreenState extends ConsumerState { final ScreenshotController _screenshotController = ScreenshotController(); void _showReceivePaymentSheet( BuildContext context, WidgetRef ref, Invoice latestInvoice, ) async { final result = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => ReceivePaymentSheet(invoice: latestInvoice), ); if (result == true) { ref.read(invoicesProvider.notifier).refresh(); } } @override Widget build(BuildContext context) { final invoicesState = ref.watch(invoicesProvider); final latestInvoice = invoicesState.value?.firstWhere( (i) => i.id == widget.invoice.id, orElse: () => widget.invoice, ) ?? widget.invoice; final customersState = ref.watch(customersProvider); final customer = customersState.value?.firstWhere( (c) => c.id == latestInvoice.customerId, orElse: () => null as dynamic, ); final businessState = ref.watch(businessProfileProvider); final business = businessState.value; // Watch products so the UI rebuilds if products are loaded asynchronously ref.watch(productsProvider); final formatCurrency = NumberFormat.currency(symbol: '₹'); final formatDate = DateFormat('dd MMM yyyy'); double remaining = latestInvoice.totalAmount - latestInvoice.amountPaid; return Scaffold( backgroundColor: Colors.grey[50], appBar: AppBar( title: Text( 'Invoice #${latestInvoice.invoiceNumber}', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), backgroundColor: Colors.white, foregroundColor: Colors.black, elevation: 0, centerTitle: true, actions: [ IconButton( icon: const Icon(LucideIcons.share2, size: 20), onPressed: () => _showShareOptions(context, latestInvoice.invoiceNumber), ), ], ), body: SingleChildScrollView( child: Screenshot( controller: _screenshotController, child: Container( color: Colors.grey[50], padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Biller Header Card (Premium Dark) Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.blue.shade900, Colors.blue.shade800], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.blue.withOpacity(0.2), blurRadius: 15, offset: const Offset(0, 5), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( business?.businessName ?? 'Your Company Name', style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(20), ), child: Text( latestInvoice.status, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12, letterSpacing: 1, ), ), ), ], ), const SizedBox(height: 16), if (business?.address != null && business!.address!.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( LucideIcons.mapPin, size: 14, color: Colors.blue.shade200, ), const SizedBox(width: 8), Expanded( child: Text( business.address!, style: TextStyle( color: Colors.blue.shade100, fontSize: 13, height: 1.4, ), ), ), ], ), ), if (business?.contactNumber != null && business!.contactNumber!.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( children: [ Icon( LucideIcons.phone, size: 14, color: Colors.blue.shade200, ), const SizedBox(width: 8), Text( business.contactNumber!, style: TextStyle( color: Colors.blue.shade100, fontSize: 13, ), ), ], ), ), if (business?.gstin != null && business!.gstin!.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( children: [ Icon( LucideIcons.building, size: 14, color: Colors.blue.shade200, ), const SizedBox(width: 8), Text( 'GSTIN: ${business.gstin}', style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold, ), ), ], ), ), ], ), ), const SizedBox(height: 20), // Invoice Dates & Customer Details Row IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Dates Expanded( flex: 2, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'INVOICE NO', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey, letterSpacing: 1, ), ), const SizedBox(height: 4), Text( latestInvoice.invoiceNumber, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.blue, ), ), const SizedBox(height: 16), const Text( 'INVOICE DATE', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey, letterSpacing: 1, ), ), const SizedBox(height: 4), Text( formatDate.format(latestInvoice.issueDate), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 16), const Text( 'DUE DATE', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey, letterSpacing: 1, ), ), const SizedBox(height: 4), Text( latestInvoice.dueDate != null ? formatDate.format(latestInvoice.dueDate!) : '-', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), ], ), ), ), const SizedBox(width: 12), // Bill To Expanded( flex: 3, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'BILL TO', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey, letterSpacing: 1, ), ), const SizedBox(height: 8), if (customer != null) ...[ Text( customer.name, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, height: 1.2, ), ), const SizedBox(height: 4), if (customer.address != null && customer.address!.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 4), child: Text( customer.address!, style: TextStyle( fontSize: 13, color: Colors.grey.shade700, height: 1.3, ), ), ), if (customer.phone != null && customer.phone!.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 4), child: Text( customer.phone!, style: TextStyle( fontSize: 13, color: Colors.grey.shade700, ), ), ), if (customer.gstin != null && customer.gstin!.isNotEmpty) Text( 'GSTIN: ${customer.gstin}', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.blue, ), ), ] else ...[ const Text( 'Walk-in Customer', style: TextStyle( fontSize: 14, fontStyle: FontStyle.italic, color: Colors.grey, ), ), ], ], ), ), ), ], ), ), const SizedBox(height: 24), const Padding( padding: EdgeInsets.symmetric(horizontal: 4), child: Text( 'Itemized Details', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ), const SizedBox(height: 12), // Item Cards instead of DataTable ...(latestInvoice.items ?? []).map((item) { final product = item.productId != null ? ref .read(productsProvider) .value ?.where((p) => p.id == item.productId) .firstOrNull : null; final skuToDisplay = item.sku ?? product?.sku; return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade200), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.03), blurRadius: 5, offset: const Offset(0, 2), ), ], ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( item.description ?? 'Item', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), Text( formatCurrency.format(item.total), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black, ), ), ], ), if (skuToDisplay != null && skuToDisplay.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 4), child: Text( 'SKU: $skuToDisplay', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, fontWeight: FontWeight.w600, ), ), ), if (item.hsnCode != null && item.hsnCode!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 4), child: Text( 'HSN: ${item.hsnCode}', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, fontWeight: FontWeight.w600, ), ), ) else if (product?.hsnCode != null && product!.hsnCode!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 4), child: Text( 'HSN: ${product.hsnCode}', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, fontWeight: FontWeight.w600, ), ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(8), ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '${item.quantity.toStringAsFixed(item.quantity.truncateToDouble() == item.quantity ? 0 : 2)} x ${formatCurrency.format(item.unitPrice)}', style: TextStyle( fontSize: 13, color: Colors.grey.shade800, ), ), Text( formatCurrency.format( item.quantity * item.unitPrice, ), style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, ), ), ], ), if (item.makingCharge > 0) Padding( padding: const EdgeInsets.only(top: 6), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '+ Making Charges', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), Text( formatCurrency.format( item.makingCharge, ), style: TextStyle( fontSize: 12, color: Colors.grey.shade700, ), ), ], ), ), if (item.otherCharges > 0) Padding( padding: const EdgeInsets.only(top: 6), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '+ Other Charges', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), Text( formatCurrency.format( item.otherCharges, ), style: TextStyle( fontSize: 12, color: Colors.grey.shade700, ), ), ], ), ), if (item.discount > 0) Padding( padding: const EdgeInsets.only(top: 6), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '- Discount', style: TextStyle( fontSize: 12, color: Colors.green.shade600, ), ), Text( '-${formatCurrency.format(item.discount)}', style: TextStyle( fontSize: 12, color: Colors.green.shade600, fontWeight: FontWeight.w600, ), ), ], ), ), if (item.taxRate > 0) Padding( padding: const EdgeInsets.only(top: 6), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '+ Tax (${item.taxRate}%)', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), Text( formatCurrency.format( item.total - (item.quantity * item.unitPrice) - item.makingCharge - item.otherCharges + item.discount, ), style: TextStyle( fontSize: 12, color: Colors.grey.shade700, ), ), ], ), ), ], ), ), ], ), ), ); }), const SizedBox(height: 12), // Summary Section Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade200), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( children: [ _buildSummaryRow( 'Subtotal', formatCurrency.format(latestInvoice.subtotal), ), if (latestInvoice.discountTotal > 0) _buildSummaryRow( 'Discount', '-${formatCurrency.format(latestInvoice.discountTotal)}', color: Colors.green.shade700, ), if (latestInvoice.taxTotal > 0) _buildSummaryRow( 'Tax', formatCurrency.format(latestInvoice.taxTotal), ), const Padding( padding: EdgeInsets.symmetric(vertical: 12), child: Divider(height: 1, color: Colors.grey), ), _buildSummaryRow( 'Grand Total', formatCurrency.format(latestInvoice.totalAmount), isBold: true, fontSize: 18, color: Colors.black, ), const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(12), ), child: Column( children: [ _buildSummaryRow( 'Amount Paid', formatCurrency.format(latestInvoice.amountPaid), color: Colors.green.shade700, isBold: true, ), const SizedBox(height: 8), _buildSummaryRow( 'Balance Due', formatCurrency.format(remaining), color: remaining > 0 ? Colors.red.shade700 : Colors.green.shade700, isBold: true, fontSize: 16, ), ], ), ), ], ), ), const SizedBox(height: 24), // Payment Info FutureBuilder>( future: ref .read(invoicesProvider.notifier) .fetchPaymentsForInvoice(latestInvoice.id!), builder: (context, snapshot) { final payments = snapshot.data ?? []; final displayMethod = payments.isNotEmpty ? payments.last.paymentMethod : latestInvoice.paymentMethod; if (displayMethod == null && !latestInvoice.isEmi) return const SizedBox.shrink(); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.blue.shade50.withOpacity(0.5), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (displayMethod != null) ...[ Row( children: [ const Icon( LucideIcons.creditCard, size: 16, color: Colors.blue, ), const SizedBox(width: 8), Text( 'Payment Mode: $displayMethod', style: const TextStyle( fontWeight: FontWeight.w600, color: Colors.blue, ), ), ], ), ], if (latestInvoice.isEmi && latestInvoice.emiAmount != null) ...[ if (displayMethod != null) const SizedBox(height: 12), Row( children: [ const Icon( LucideIcons.calendarClock, size: 16, color: Colors.purple, ), const SizedBox(width: 8), Text( 'EMI: ${formatCurrency.format(latestInvoice.emiAmount)} / ${latestInvoice.emiCycle}', style: const TextStyle( fontWeight: FontWeight.w600, color: Colors.purple, ), ), ], ), if (latestInvoice.nextPaymentDate != null && remaining > 0) Padding( padding: const EdgeInsets.only( left: 24, top: 4, ), child: Text( 'Next Due: ${formatDate.format(latestInvoice.nextPaymentDate!)}', style: TextStyle( fontSize: 13, color: Colors.grey.shade700, ), ), ), ], ], ), ); }, ), // Bottom Spacing for FAB const SizedBox(height: 100), ], ), ), ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: (remaining > 0 && latestInvoice.status != 'DRAFT') ? Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: SizedBox( width: double.infinity, child: FloatingActionButton.extended( onPressed: () => _showReceivePaymentSheet(context, ref, latestInvoice), icon: const Icon(LucideIcons.indianRupee), label: const Text( 'Receive Payment', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), backgroundColor: Colors.blue, elevation: 4, ), ), ) : null, ); } Widget _buildSummaryRow( String label, String value, { bool isBold = false, Color? color, double fontSize = 14, }) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle( fontSize: fontSize, fontWeight: isBold ? FontWeight.bold : FontWeight.w500, color: color ?? Colors.grey.shade600, ), ), Text( value, style: TextStyle( fontSize: fontSize, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: color ?? Colors.black87, ), ), ], ), ); } void _showShareOptions(BuildContext context, String invoiceNumber) { showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) => SafeArea( child: Wrap( children: [ Padding( padding: const EdgeInsets.all(16.0), child: Text( 'Share Invoice $invoiceNumber', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ListTile( leading: const Icon(LucideIcons.image, color: Colors.blue), title: const Text('Share as Image'), subtitle: const Text('Best for WhatsApp, precise look'), onTap: () { Navigator.pop(ctx); _shareAsImage(invoiceNumber); }, ), ListTile( leading: const Icon(LucideIcons.fileText, color: Colors.red), title: const Text('Share as PDF'), subtitle: const Text('Professional document format'), onTap: () { Navigator.pop(ctx); _shareAsPdf(invoiceNumber); }, ), const SizedBox(height: 16), ], ), ), ); } Future _shareAsImage(String invoiceNumber) async { try { final Uint8List? image = await _screenshotController.capture( pixelRatio: 3.0, ); if (image == null) return; final directory = await getTemporaryDirectory(); final imagePath = await File( '${directory.path}/Invoice_$invoiceNumber.png', ).create(); await imagePath.writeAsBytes(image); await Share.shareXFiles([ XFile(imagePath.path), ], text: 'Invoice $invoiceNumber'); } catch (e) { if (mounted) ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Error sharing image: $e'))); } } Future _shareAsPdf(String invoiceNumber) async { try { final invoicesState = ref.read(invoicesProvider); final latestInvoice = invoicesState.value?.firstWhere( (i) => i.id == widget.invoice.id, orElse: () => widget.invoice, ) ?? widget.invoice; final customers = ref.read(customersProvider).value ?? []; final customer = customers.where((c) => c.id == latestInvoice.customerId).isEmpty ? null : customers.firstWhere((c) => c.id == latestInvoice.customerId); final businessState = ref.read(businessProfileProvider); final business = businessState.value; final formatCurrency = NumberFormat.currency( symbol: 'Rs. ', decimalDigits: 2, ); final formatDate = DateFormat('dd MMM yyyy'); // Fetch payment info final payments = await ref .read(invoicesProvider.notifier) .fetchPaymentsForInvoice(latestInvoice.id!); final displayMethod = payments.isNotEmpty ? payments.last.paymentMethod : latestInvoice.paymentMethod; final pdf = pw.Document(); pdf.addPage( pw.MultiPage( pageFormat: PdfPageFormat.a4, margin: const pw.EdgeInsets.all(32), build: (pw.Context context) { return [ // Header pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( business?.businessName ?? 'Your Company', style: pw.TextStyle( fontSize: 24, fontWeight: pw.FontWeight.bold, ), ), pw.SizedBox(height: 4), if (business?.address != null) pw.Text( business!.address!, style: const pw.TextStyle(fontSize: 12), ), if (business?.contactNumber != null) pw.Text( 'Phone: ${business!.contactNumber}', style: const pw.TextStyle(fontSize: 12), ), if (business?.gstin != null) pw.Text( 'GSTIN: ${business!.gstin}', style: pw.TextStyle( fontSize: 12, fontWeight: pw.FontWeight.bold, ), ), ], ), pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.end, children: [ pw.Text( 'INVOICE', style: pw.TextStyle( fontSize: 28, fontWeight: pw.FontWeight.bold, color: PdfColors.blue800, ), ), pw.SizedBox(height: 8), pw.Text( latestInvoice.invoiceNumber, style: pw.TextStyle( fontSize: 16, fontWeight: pw.FontWeight.bold, ), ), pw.Text( 'Date: ${formatDate.format(latestInvoice.issueDate)}', style: const pw.TextStyle(fontSize: 12), ), if (latestInvoice.dueDate != null) pw.Text( 'Due Date: ${formatDate.format(latestInvoice.dueDate!)}', style: const pw.TextStyle(fontSize: 12), ), pw.SizedBox(height: 4), pw.Container( padding: const pw.EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: pw.BoxDecoration( color: PdfColors.grey200, borderRadius: const pw.BorderRadius.all( pw.Radius.circular(4), ), ), child: pw.Text( latestInvoice.status, style: pw.TextStyle( fontSize: 12, fontWeight: pw.FontWeight.bold, ), ), ), ], ), ], ), pw.SizedBox(height: 32), // Bill To pw.Text( 'BILL TO:', style: pw.TextStyle( fontSize: 12, fontWeight: pw.FontWeight.bold, color: PdfColors.grey600, ), ), pw.SizedBox(height: 4), if (customer != null) ...[ pw.Text( customer.name, style: pw.TextStyle( fontSize: 16, fontWeight: pw.FontWeight.bold, ), ), if (customer.address != null) pw.Text( customer.address!, style: const pw.TextStyle(fontSize: 12), ), if (customer.phone != null) pw.Text( 'Phone: ${customer.phone}', style: const pw.TextStyle(fontSize: 12), ), if (customer.gstin != null) pw.Text( 'GSTIN: ${customer.gstin}', style: pw.TextStyle( fontSize: 12, fontWeight: pw.FontWeight.bold, ), ), ] else ...[ pw.Text( 'Walk-in Customer', style: const pw.TextStyle(fontSize: 14), ), ], pw.SizedBox(height: 32), // Items Table pw.TableHelper.fromTextArray( context: context, border: const pw.TableBorder( bottom: pw.BorderSide(color: PdfColors.grey300, width: .5), horizontalInside: pw.BorderSide( color: PdfColors.grey300, width: .5, ), ), headerStyle: pw.TextStyle( fontWeight: pw.FontWeight.bold, color: PdfColors.white, ), headerDecoration: const pw.BoxDecoration( color: PdfColors.blue800, ), cellAlignments: { 0: pw.Alignment.centerLeft, 1: pw.Alignment.centerRight, 2: pw.Alignment.centerRight, 3: pw.Alignment.centerRight, }, data: [ ['Description', 'Qty', 'Unit Price', 'Total'], ...(latestInvoice.items ?? []).map((item) { final product = item.productId != null ? ref .read(productsProvider) .value ?.where((p) => p.id == item.productId) .firstOrNull : null; final skuToDisplay = item.sku ?? product?.sku; final hsnToDisplay = item.hsnCode ?? product?.hsnCode; String itemDesc = item.description ?? 'Item'; if (skuToDisplay != null && skuToDisplay.isNotEmpty) itemDesc += '\nSKU: $skuToDisplay'; if (hsnToDisplay != null && hsnToDisplay.isNotEmpty) itemDesc += '\nHSN: $hsnToDisplay'; if (item.makingCharge > 0) itemDesc += '\n+ Making Charges: ${formatCurrency.format(item.makingCharge)}'; if (item.otherCharges > 0) itemDesc += '\n+ Other Charges: ${formatCurrency.format(item.otherCharges)}'; if (item.discount > 0) itemDesc += '\n- Discount: ${formatCurrency.format(item.discount)}'; if (item.taxRate > 0) { final taxAmt = item.total - (item.quantity * item.unitPrice) - item.makingCharge - item.otherCharges + item.discount; itemDesc += '\n+ Tax (${item.taxRate}%): ${formatCurrency.format(taxAmt)}'; } return [ itemDesc, item.quantity.toStringAsFixed( item.quantity.truncateToDouble() == item.quantity ? 0 : 2, ), formatCurrency.format(item.unitPrice), formatCurrency.format(item.total), ]; }), ], ), pw.SizedBox(height: 24), // Totals pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ pw.Container( width: 250, child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.stretch, children: [ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('Subtotal:'), pw.Text( formatCurrency.format(latestInvoice.subtotal), ), ], ), pw.SizedBox(height: 4), if (latestInvoice.discountTotal > 0) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('Discount:'), pw.Text( '-${formatCurrency.format(latestInvoice.discountTotal)}', style: const pw.TextStyle( color: PdfColors.green700, ), ), ], ), pw.SizedBox(height: 4), ], if (latestInvoice.taxTotal > 0) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('Tax:'), pw.Text( formatCurrency.format(latestInvoice.taxTotal), ), ], ), pw.SizedBox(height: 4), ], pw.Divider(color: PdfColors.grey400), pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text( 'Grand Total:', style: pw.TextStyle( fontWeight: pw.FontWeight.bold, fontSize: 16, ), ), pw.Text( formatCurrency.format(latestInvoice.totalAmount), style: pw.TextStyle( fontWeight: pw.FontWeight.bold, fontSize: 16, ), ), ], ), pw.SizedBox(height: 12), pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text( 'Amount Paid:', style: pw.TextStyle(color: PdfColors.green700), ), pw.Text( formatCurrency.format(latestInvoice.amountPaid), style: pw.TextStyle(color: PdfColors.green700), ), ], ), pw.Divider(color: PdfColors.grey400), pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text( 'Balance Due:', style: pw.TextStyle( fontWeight: pw.FontWeight.bold, fontSize: 14, ), ), pw.Text( formatCurrency.format( latestInvoice.totalAmount - latestInvoice.amountPaid, ), style: pw.TextStyle( fontWeight: pw.FontWeight.bold, fontSize: 14, ), ), ], ), ], ), ), ], ), pw.SizedBox(height: 24), if (displayMethod != null || latestInvoice.isEmi) pw.Container( padding: const pw.EdgeInsets.all(12), decoration: pw.BoxDecoration( color: PdfColors.blue50, borderRadius: const pw.BorderRadius.all( pw.Radius.circular(8), ), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ if (displayMethod != null) pw.Text( 'Payment Mode: $displayMethod', style: pw.TextStyle( fontWeight: pw.FontWeight.bold, color: PdfColors.blue800, ), ), if (latestInvoice.isEmi && latestInvoice.emiAmount != null) ...[ if (displayMethod != null) pw.SizedBox(height: 4), pw.Text( 'EMI: ${formatCurrency.format(latestInvoice.emiAmount)} / ${latestInvoice.emiCycle}', style: pw.TextStyle( fontWeight: pw.FontWeight.bold, color: PdfColors.purple800, ), ), if (latestInvoice.nextPaymentDate != null && (latestInvoice.totalAmount - latestInvoice.amountPaid) > 0) pw.Text( 'Next Due: ${formatDate.format(latestInvoice.nextPaymentDate!)}', style: const pw.TextStyle( fontSize: 12, color: PdfColors.grey700, ), ), ], ], ), ), pw.SizedBox(height: 40), // Footer pw.Divider(color: PdfColors.grey300), pw.SizedBox(height: 8), pw.Center( child: pw.Text( 'Thank you for your business!', style: pw.TextStyle( color: PdfColors.grey600, fontStyle: pw.FontStyle.italic, ), ), ), ]; }, ), ); final directory = await getTemporaryDirectory(); final pdfPath = await File( '${directory.path}/Invoice_$invoiceNumber.pdf', ).create(); await pdfPath.writeAsBytes(await pdf.save()); await Share.shareXFiles([ XFile(pdfPath.path), ], text: 'Invoice $invoiceNumber'); } catch (e) { if (mounted) ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Error sharing PDF: $e'))); } } }