Files
Kifi/kifi-app/lib/features/sales/presentation/invoice_details_screen.dart
2026-08-24 18:38:46 +05:30

802 lines
40 KiB
Dart

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<InvoiceDetailsScreen> createState() => _InvoiceDetailsScreenState();
}
class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
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<List<InvoicePayment>>(
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<void> _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<void> _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')));
}
}
}