Files
Kifi/kifi-app/lib/features/sales/presentation/invoice_details_screen.dart

1398 lines
58 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')));
}
}
}