797 lines
38 KiB
Dart
797 lines
38 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 '../../inventory/providers/product_categories_provider.dart';
|
|
import '../domain/invoice.dart';
|
|
import '../providers/invoices_provider.dart';
|
|
import '../providers/customers_provider.dart';
|
|
import '../../business/providers/business_provider.dart';
|
|
import '../../transactions/presentation/widgets/attachment_gallery_screen.dart';
|
|
import '../../../core/network/dio_client.dart';
|
|
import 'widgets/receive_payment_sheet.dart';
|
|
import 'invoice_builder_screen.dart';
|
|
|
|
String _resolveImageUrl(String path) {
|
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
return path;
|
|
}
|
|
final base = DioClient().dio.options.baseUrl;
|
|
final cleanPath = path.startsWith('/') ? path.substring(1) : path;
|
|
if (cleanPath.startsWith('api/kifi-v2/upload/view') || cleanPath.startsWith('upload/view')) {
|
|
return '$base/${cleanPath.replaceFirst('api/kifi-v2/', '')}';
|
|
}
|
|
return '$base/upload/view?path=${Uri.encodeComponent(cleanPath)}';
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
void _editInvoice(Invoice invoice) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => InvoiceBuilderScreen(existingInvoice: invoice),
|
|
),
|
|
);
|
|
}
|
|
|
|
@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?.where((c) => c.id == latestInvoice.customerId).firstOrNull;
|
|
|
|
final businessState = ref.watch(businessProfileProvider);
|
|
final business = businessState.value;
|
|
|
|
final products = ref.watch(productsProvider).value ?? [];
|
|
final categories = ref.watch(productCategoriesProvider).value ?? [];
|
|
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
final formatCurrency = NumberFormat.currency(symbol: '₹');
|
|
final formatDate = DateFormat('dd MMM yyyy');
|
|
|
|
double remaining = latestInvoice.totalAmount - latestInvoice.amountPaid;
|
|
|
|
return Scaffold(
|
|
backgroundColor: isDark ? const Color(0xFF0F172A) : Colors.grey[50],
|
|
appBar: AppBar(
|
|
title: Text(
|
|
'Invoice #${latestInvoice.invoiceNumber}',
|
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
centerTitle: true,
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(LucideIcons.edit3, size: 20),
|
|
tooltip: 'Edit Invoice',
|
|
onPressed: () => _editInvoice(latestInvoice),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(LucideIcons.share2, size: 20),
|
|
tooltip: 'Share Invoice',
|
|
onPressed: () => _showShareOptions(context, latestInvoice.invoiceNumber),
|
|
),
|
|
],
|
|
),
|
|
body: SingleChildScrollView(
|
|
child: Screenshot(
|
|
controller: _screenshotController,
|
|
child: Container(
|
|
color: isDark ? const Color(0xFF0F172A) : Colors.grey[50],
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Top Header Card: Business info
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: isDark ? Colors.white12 : Colors.grey.shade200),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
business?.businessName ?? 'KIFI JEWELLERS',
|
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
if (business?.address != null)
|
|
Text(business!.address!, style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
|
|
if (business?.gstin != null)
|
|
Text('GSTIN: ${business!.gstin}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.blue)),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: _getStatusColor(latestInvoice.status).withValues(alpha: 0.15),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
latestInvoice.status,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
color: _getStatusColor(latestInvoice.status),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Invoice metadata & Customer details
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Invoice metadata
|
|
Expanded(
|
|
flex: 2,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: isDark ? Colors.white12 : Colors.grey.shade200),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('INVOICE NO', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey)),
|
|
const SizedBox(height: 2),
|
|
Text(latestInvoice.invoiceNumber, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: Colors.blue)),
|
|
const SizedBox(height: 10),
|
|
const Text('INVOICE DATE', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey)),
|
|
const SizedBox(height: 2),
|
|
Text(formatDate.format(latestInvoice.issueDate), style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
// Customer Details
|
|
Expanded(
|
|
flex: 3,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: isDark ? Colors.white12 : Colors.grey.shade200),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('BILL TO', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey)),
|
|
const SizedBox(height: 4),
|
|
if (customer != null) ...[
|
|
Text(customer.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
|
if (customer.phone != null)
|
|
Text('Ph: ${customer.phone}', style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
|
|
if (customer.gstin != null)
|
|
Text('GSTIN: ${customer.gstin}', style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.blue)),
|
|
] else ...[
|
|
const Text('Walk-in Customer', style: TextStyle(fontSize: 13, fontStyle: FontStyle.italic, color: Colors.grey)),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Itemized Details Header
|
|
const Text('Itemized Particulars', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 10),
|
|
|
|
// Item Cards
|
|
...(latestInvoice.items).map((item) {
|
|
final product = products.where((p) => p.id == item.productId).firstOrNull;
|
|
final category = categories.where((c) => c.id == product?.categoryId).firstOrNull;
|
|
final weight = item.weight ?? item.quantity;
|
|
final metalAmount = weight * item.unitPrice;
|
|
|
|
double makingChargeAmt = 0.0;
|
|
if (item.makingChargesType == 'PERCENTAGE') {
|
|
makingChargeAmt = metalAmount * (item.makingCharge / 100.0);
|
|
} else if (item.makingChargesType == 'PER_PIECE') {
|
|
makingChargeAmt = item.makingCharge;
|
|
} else {
|
|
makingChargeAmt = weight * item.makingCharge;
|
|
}
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: isDark ? Colors.white12 : Colors.grey.shade200),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (item.photoUrl != null && item.photoUrl!.isNotEmpty)
|
|
Container(
|
|
width: 44,
|
|
height: 44,
|
|
margin: const EdgeInsets.only(right: 12),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(8),
|
|
image: DecorationImage(
|
|
image: NetworkImage(_resolveImageUrl(item.photoUrl!)),
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
item.productName ?? product?.name ?? item.description ?? 'Jewellery Item',
|
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Wrap(
|
|
spacing: 6,
|
|
runSpacing: 4,
|
|
children: [
|
|
if (item.categoryName != null || category?.name != null)
|
|
_buildDetailBadge(item.categoryName ?? category!.name, const Color(0xFFD4AF37)),
|
|
if (item.sku != null || product?.sku != null)
|
|
_buildDetailBadge('SKU: ${item.sku ?? product!.sku}', Colors.blue),
|
|
if (item.huid != null)
|
|
_buildDetailBadge('HUID: ${item.huid}', Colors.purple),
|
|
if (item.hsnCode != null)
|
|
_buildDetailBadge('HSN: ${item.hsnCode}', Colors.grey),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Text(
|
|
formatCurrency.format(item.total),
|
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.green),
|
|
),
|
|
],
|
|
),
|
|
const Divider(height: 18),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Metal (${weight.toStringAsFixed(3)}g @ ₹${item.unitPrice.toStringAsFixed(2)}/g):',
|
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
|
),
|
|
Text(formatCurrency.format(metalAmount), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12)),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Making Charges (${item.makingChargesType == 'PERCENTAGE' ? '${item.makingCharge}%' : (item.makingChargesType == 'PER_PIECE' ? '₹${item.makingCharge}/pc' : '₹${item.makingCharge}/g')}):',
|
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
|
),
|
|
Text(formatCurrency.format(makingChargeAmt), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12, color: Colors.indigo)),
|
|
],
|
|
),
|
|
if (item.taxRate > 0) ...[
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text('GST (${item.taxRate.toStringAsFixed(1)}%):', style: TextStyle(fontSize: 11, color: Colors.grey.shade500)),
|
|
Text(
|
|
'+ ${formatCurrency.format(((metalAmount + makingChargeAmt) * (item.taxRate / 100.0)))}',
|
|
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// Grand Totals Summary
|
|
Container(
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: isDark ? Colors.white12 : Colors.grey.shade200),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
_buildSummaryRow('Subtotal', formatCurrency.format(latestInvoice.subtotal)),
|
|
if (latestInvoice.cgstTotal > 0 && latestInvoice.sgstTotal > 0) ...[
|
|
_buildSummaryRow('CGST', formatCurrency.format(latestInvoice.cgstTotal)),
|
|
_buildSummaryRow('SGST', formatCurrency.format(latestInvoice.sgstTotal)),
|
|
] else if (latestInvoice.igstTotal > 0) ...[
|
|
_buildSummaryRow('IGST', formatCurrency.format(latestInvoice.igstTotal)),
|
|
] else if (latestInvoice.taxTotal > 0) ...[
|
|
_buildSummaryRow('Tax', formatCurrency.format(latestInvoice.taxTotal)),
|
|
],
|
|
if (latestInvoice.discountTotal > 0)
|
|
_buildSummaryRow('Discount', '-${formatCurrency.format(latestInvoice.discountTotal)}', color: Colors.green),
|
|
const Divider(height: 20),
|
|
_buildSummaryRow('Grand Total', formatCurrency.format(latestInvoice.totalAmount), isBold: true, fontSize: 18),
|
|
const SizedBox(height: 10),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? Colors.black26 : Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
_buildSummaryRow('Amount Received', formatCurrency.format(latestInvoice.amountPaid), color: Colors.green, isBold: true),
|
|
const SizedBox(height: 6),
|
|
_buildSummaryRow('Balance Due', formatCurrency.format(remaining), color: remaining > 0 ? Colors.red : Colors.green, isBold: true, fontSize: 15),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Attached Invoice Bill if available
|
|
if (latestInvoice.invoiceUrl != null && latestInvoice.invoiceUrl!.isNotEmpty) ...[
|
|
const Text('Attached Document', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 8),
|
|
ListTile(
|
|
tileColor: isDark ? const Color(0xFF1E293B) : Colors.white,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
leading: const Icon(LucideIcons.image, color: Colors.blue),
|
|
title: const Text('View Attached Sales Slip'),
|
|
subtitle: const Text('Tap to open photo gallery', style: TextStyle(fontSize: 12, color: Colors.blue)),
|
|
onTap: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => AttachmentGalleryScreen(
|
|
images: [NetworkImage(_resolveImageUrl(latestInvoice.invoiceUrl!))],
|
|
initialIndex: 0,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
],
|
|
|
|
// Bottom Action: Receive Payment if balance due
|
|
if (remaining > 0)
|
|
ElevatedButton.icon(
|
|
onPressed: () => _showReceivePaymentSheet(context, ref, latestInvoice),
|
|
icon: const Icon(LucideIcons.plusCircle, size: 18),
|
|
label: const Text('Record Payment', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
|
style: ElevatedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
backgroundColor: Colors.green,
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDetailBadge(String text, Color color) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: color.withValues(alpha: 0.3), width: 0.5),
|
|
),
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: color),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSummaryRow(String label, String value, {bool isBold = false, double fontSize = 14, Color? color}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 3),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(label, style: TextStyle(fontSize: fontSize, fontWeight: isBold ? FontWeight.bold : FontWeight.normal)),
|
|
Text(value, style: TextStyle(fontSize: fontSize, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: color)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Color _getStatusColor(String status) {
|
|
switch (status) {
|
|
case 'PAID':
|
|
return Colors.green;
|
|
case 'PARTIAL':
|
|
return Colors.blue;
|
|
case 'FINALIZED':
|
|
return Colors.orange;
|
|
case 'DRAFT':
|
|
default:
|
|
return Colors.grey;
|
|
}
|
|
}
|
|
|
|
void _showShareOptions(BuildContext context, String invoiceNumber) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
|
|
builder: (ctx) => SafeArea(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: Text('Share Invoice', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(LucideIcons.image, color: Colors.blue),
|
|
title: const Text('Share as Image'),
|
|
subtitle: const Text('High-resolution receipt preview'),
|
|
onTap: () {
|
|
Navigator.pop(ctx);
|
|
_shareAsImage(invoiceNumber);
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(LucideIcons.fileText, color: Colors.red),
|
|
title: const Text('Share as PDF (Tax Invoice)'),
|
|
subtitle: const Text('Official jewellery tax invoice 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).firstOrNull;
|
|
|
|
final business = ref.read(businessProfileProvider).value;
|
|
final formatCurrency = NumberFormat.currency(symbol: '₹ ', decimalDigits: 2);
|
|
final formatDate = DateFormat('dd MMM yyyy');
|
|
|
|
final pdf = pw.Document();
|
|
|
|
pdf.addPage(
|
|
pw.MultiPage(
|
|
pageFormat: PdfPageFormat.a4,
|
|
margin: const pw.EdgeInsets.all(28),
|
|
build: (pw.Context context) {
|
|
return [
|
|
// Header with Business Details & TAX INVOICE title
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Text(
|
|
business?.businessName ?? 'KIFI JEWELLERS',
|
|
style: pw.TextStyle(fontSize: 22, fontWeight: pw.FontWeight.bold, color: PdfColors.blue900),
|
|
),
|
|
pw.SizedBox(height: 4),
|
|
if (business?.address != null)
|
|
pw.Text(business!.address!, style: const pw.TextStyle(fontSize: 10)),
|
|
if (business?.contactNumber != null)
|
|
pw.Text('Phone: ${business!.contactNumber}', style: const pw.TextStyle(fontSize: 10)),
|
|
if (business?.gstin != null)
|
|
pw.Text('GSTIN: ${business!.gstin}', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.blue800)),
|
|
],
|
|
),
|
|
pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
|
children: [
|
|
pw.Text(
|
|
'TAX INVOICE',
|
|
style: pw.TextStyle(fontSize: 24, fontWeight: pw.FontWeight.bold, color: PdfColors.blue900),
|
|
),
|
|
pw.SizedBox(height: 4),
|
|
pw.Text('Invoice #: ${latestInvoice.invoiceNumber}', style: pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold)),
|
|
pw.Text('Date: ${formatDate.format(latestInvoice.issueDate)}', style: const pw.TextStyle(fontSize: 10)),
|
|
if (latestInvoice.dueDate != null)
|
|
pw.Text('Due Date: ${formatDate.format(latestInvoice.dueDate!)}', style: const pw.TextStyle(fontSize: 10)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
pw.SizedBox(height: 20),
|
|
|
|
// Bill To Box
|
|
pw.Container(
|
|
padding: const pw.EdgeInsets.all(10),
|
|
decoration: pw.BoxDecoration(
|
|
border: pw.Border.all(color: PdfColors.grey400, width: 0.5),
|
|
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6)),
|
|
),
|
|
child: pw.Row(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Expanded(
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Text('BILL TO (CUSTOMER):', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)),
|
|
pw.SizedBox(height: 2),
|
|
pw.Text(customer?.name ?? 'Walk-in Customer', style: pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold)),
|
|
if (customer?.phone != null) pw.Text('Phone: ${customer!.phone}', style: const pw.TextStyle(fontSize: 9)),
|
|
if (customer?.address != null) pw.Text('Address: ${customer!.address}', style: const pw.TextStyle(fontSize: 9)),
|
|
if (customer?.gstin != null) pw.Text('GSTIN: ${customer!.gstin}', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
pw.SizedBox(height: 16),
|
|
|
|
// Table of Items with HUID, SKU, Making Charges, etc.
|
|
pw.TableHelper.fromTextArray(
|
|
context: context,
|
|
border: const pw.TableBorder(
|
|
bottom: pw.BorderSide(color: PdfColors.grey300, width: 0.5),
|
|
horizontalInside: pw.BorderSide(color: PdfColors.grey300, width: 0.5),
|
|
),
|
|
headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold, color: PdfColors.white, fontSize: 9),
|
|
headerDecoration: const pw.BoxDecoration(color: PdfColors.blue900),
|
|
cellStyle: const pw.TextStyle(fontSize: 8.5),
|
|
cellAlignments: {
|
|
0: pw.Alignment.centerLeft,
|
|
1: pw.Alignment.centerLeft,
|
|
2: pw.Alignment.centerRight,
|
|
3: pw.Alignment.centerRight,
|
|
4: pw.Alignment.centerRight,
|
|
5: pw.Alignment.centerRight,
|
|
6: pw.Alignment.centerRight,
|
|
},
|
|
headers: ['Item / Particulars', 'HSN / HUID', 'Weight', 'Rate', 'Making Chg', 'GST', 'Total'],
|
|
data: latestInvoice.items.map((item) {
|
|
final weight = item.weight ?? item.quantity;
|
|
final metalAmt = weight * item.unitPrice;
|
|
|
|
double makingAmt = 0.0;
|
|
if (item.makingChargesType == 'PERCENTAGE') {
|
|
makingAmt = metalAmt * (item.makingCharge / 100.0);
|
|
} else if (item.makingChargesType == 'PER_PIECE') {
|
|
makingAmt = item.makingCharge;
|
|
} else {
|
|
makingAmt = weight * item.makingCharge;
|
|
}
|
|
|
|
String particulars = item.productName ?? item.description ?? 'Item';
|
|
if (item.sku != null && item.sku!.isNotEmpty) particulars += '\nSKU: ${item.sku}';
|
|
if (item.categoryName != null) particulars += ' (${item.categoryName})';
|
|
|
|
String hsnHuid = 'HSN: ${item.hsnCode ?? '-'}';
|
|
if (item.huid != null && item.huid!.isNotEmpty) hsnHuid += '\nHUID: ${item.huid}';
|
|
|
|
return [
|
|
particulars,
|
|
hsnHuid,
|
|
'${weight.toStringAsFixed(3)} g',
|
|
formatCurrency.format(item.unitPrice),
|
|
formatCurrency.format(makingAmt),
|
|
'${item.taxRate.toStringAsFixed(1)}%',
|
|
formatCurrency.format(item.total),
|
|
];
|
|
}).toList(),
|
|
),
|
|
pw.SizedBox(height: 16),
|
|
|
|
// Summary Section
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.end,
|
|
children: [
|
|
pw.Container(
|
|
width: 240,
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
|
children: [
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.Text('Metal Subtotal:', style: const pw.TextStyle(fontSize: 10)),
|
|
pw.Text(formatCurrency.format(latestInvoice.subtotal), style: const pw.TextStyle(fontSize: 10)),
|
|
],
|
|
),
|
|
if (latestInvoice.cgstTotal > 0 && latestInvoice.sgstTotal > 0) ...[
|
|
pw.SizedBox(height: 2),
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.Text('CGST:', style: const pw.TextStyle(fontSize: 9)),
|
|
pw.Text(formatCurrency.format(latestInvoice.cgstTotal), style: const pw.TextStyle(fontSize: 9)),
|
|
],
|
|
),
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.Text('SGST:', style: const pw.TextStyle(fontSize: 9)),
|
|
pw.Text(formatCurrency.format(latestInvoice.sgstTotal), style: const pw.TextStyle(fontSize: 9)),
|
|
],
|
|
),
|
|
] else if (latestInvoice.igstTotal > 0) ...[
|
|
pw.SizedBox(height: 2),
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.Text('IGST:', style: const pw.TextStyle(fontSize: 9)),
|
|
pw.Text(formatCurrency.format(latestInvoice.igstTotal), style: const pw.TextStyle(fontSize: 9)),
|
|
],
|
|
),
|
|
],
|
|
if (latestInvoice.discountTotal > 0) ...[
|
|
pw.SizedBox(height: 2),
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.Text('Discount:', style: const pw.TextStyle(fontSize: 9, color: PdfColors.green700)),
|
|
pw.Text('-${formatCurrency.format(latestInvoice.discountTotal)}', style: const pw.TextStyle(fontSize: 9, color: PdfColors.green700)),
|
|
],
|
|
),
|
|
],
|
|
pw.Divider(color: PdfColors.grey400, height: 10),
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.Text('Grand Total:', style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 13)),
|
|
pw.Text(formatCurrency.format(latestInvoice.totalAmount), style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 13)),
|
|
],
|
|
),
|
|
pw.SizedBox(height: 4),
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.Text('Amount Received:', style: const pw.TextStyle(fontSize: 10, color: PdfColors.green800)),
|
|
pw.Text(formatCurrency.format(latestInvoice.amountPaid), style: const pw.TextStyle(fontSize: 10, color: PdfColors.green800)),
|
|
],
|
|
),
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.Text('Balance Due:', style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 11, color: PdfColors.red800)),
|
|
pw.Text(formatCurrency.format(latestInvoice.totalAmount - latestInvoice.amountPaid), style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 11, color: PdfColors.red800)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
pw.SizedBox(height: 30),
|
|
|
|
// Terms & Signatory
|
|
pw.Divider(color: PdfColors.grey300),
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
|
children: [
|
|
pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Text('Terms & Conditions:', style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 8)),
|
|
pw.Text('1. Goods once sold are subject to standard hallmarking certification.', style: const pw.TextStyle(fontSize: 7, color: PdfColors.grey700)),
|
|
pw.Text('2. Weight & purity tested under industry standard electronic balance.', style: const pw.TextStyle(fontSize: 7, color: PdfColors.grey700)),
|
|
],
|
|
),
|
|
pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
children: [
|
|
pw.Text('For ${business?.businessName ?? "KIFI JEWELLERS"}', style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 9)),
|
|
pw.SizedBox(height: 24),
|
|
pw.Text('Authorized Signatory', style: const pw.TextStyle(fontSize: 8, color: PdfColors.grey700)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
];
|
|
},
|
|
),
|
|
);
|
|
|
|
final directory = await getTemporaryDirectory();
|
|
final pdfPath = await File('${directory.path}/TaxInvoice_$invoiceNumber.pdf').create();
|
|
await pdfPath.writeAsBytes(await pdf.save());
|
|
|
|
await Share.shareXFiles([XFile(pdfPath.path)], text: 'Tax Invoice $invoiceNumber');
|
|
} catch (e) {
|
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error sharing PDF: $e')));
|
|
}
|
|
}
|
|
}
|