790 lines
39 KiB
Dart
790 lines
39 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)),
|
|
),
|
|
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;
|
|
String itemDesc = item.description ?? 'Item';
|
|
if (skuToDisplay != null && skuToDisplay.isNotEmpty) itemDesc += '\nSKU: $skuToDisplay';
|
|
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')));
|
|
}
|
|
}
|
|
}
|