Added market place specific feature

This commit is contained in:
2026-09-04 10:43:23 +05:30
parent b123a34e8f
commit 5e9b3f2122
31 changed files with 4978 additions and 214 deletions

View File

@@ -18,9 +18,16 @@ import '../domain/invoice.dart';
import '../providers/invoices_provider.dart';
import '../providers/customers_provider.dart';
import '../../business/providers/business_provider.dart';
import '../../business/providers/indian_states_provider.dart';
import '../../transactions/presentation/widgets/attachment_gallery_screen.dart';
import '../../../core/network/dio_client.dart';
import 'widgets/receive_payment_sheet.dart';
import 'widgets/create_credit_note_sheet.dart';
import 'widgets/settle_marketplace_order_sheet.dart';
import 'credit_notes_list_screen.dart';
import 'marketplace_settlements_screen.dart';
import '../providers/credit_notes_provider.dart';
import '../providers/marketplace_settlements_provider.dart';
import 'invoice_builder_screen.dart';
bool _isCommodityProduct({
@@ -533,19 +540,239 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
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)),
),
// Credit Notes / Returns associated with this Invoice
if (latestInvoice.id != null) ...[
ref.watch(invoiceCreditNotesProvider(latestInvoice.id!)).when(
data: (cns) {
if (cns.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Returns & Credit Notes Issued', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
...cns.map((cn) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.withValues(alpha: isDark ? 0.1 : 0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red.withValues(alpha: 0.25)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(LucideIcons.undo2, color: Colors.red, size: 16),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Credit Note #${cn.creditNoteNumber}',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
Text(
'${cn.reason.replaceAll("_", " ")}${formatDate.format(cn.returnDate)}',
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
),
],
),
],
),
Text(
formatCurrency.format(cn.totalAmount),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.red),
),
],
),
);
}),
const SizedBox(height: 16),
],
);
},
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
),
],
// Marketplace Settlement Breakdown Card (if settled)
if (latestInvoice.id != null) ...[
ref.watch(invoiceSettlementProvider(latestInvoice.id!)).when(
data: (settlement) {
if (settlement == null) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Marketplace Settlement & Payout', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFF10B981).withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(6),
),
child: const Row(
children: [
Icon(LucideIcons.checkCheck, size: 12, color: Color(0xFF10B981)),
SizedBox(width: 4),
Text('Reconciled', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Color(0xFF10B981))),
],
),
),
],
),
const SizedBox(height: 8),
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.white10 : Colors.grey.shade200),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${settlement.channelName ?? "Marketplace"} Remittance', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
Text(formatDate.format(settlement.settlementDate), style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
],
),
if (settlement.settlementRef != null) ...[
const SizedBox(height: 4),
Row(
children: [
const Icon(LucideIcons.hash, size: 12, color: Colors.grey),
const SizedBox(width: 4),
Text('Bank UTR: ${settlement.settlementRef}', style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),
],
),
],
const Divider(height: 16),
_buildSummaryRow('Gross Sale Value', formatCurrency.format(settlement.grossAmount)),
if (settlement.commissionFee > 0)
_buildSummaryRow('Referral / Commission Fee', '- ${formatCurrency.format(settlement.commissionFee)}', color: Colors.red),
if (settlement.shippingFee > 0)
_buildSummaryRow('Logistics / Shipping Fee', '- ${formatCurrency.format(settlement.shippingFee)}', color: Colors.red),
if (settlement.otherFees > 0)
_buildSummaryRow('Closing / Fixed Fees', '- ${formatCurrency.format(settlement.otherFees)}', color: Colors.red),
if (settlement.feeGst > 0)
_buildSummaryRow('GST on Marketplace Fees (18%)', '- ${formatCurrency.format(settlement.feeGst)}', color: Colors.red),
if (settlement.tcsGstAmount > 0)
_buildSummaryRow('GST TCS Withheld (1%)', '- ${formatCurrency.format(settlement.tcsGstAmount)}', color: Colors.red),
if (settlement.tdsAmount > 0)
_buildSummaryRow('TDS u/s 194O Withheld (0.1%)', '- ${formatCurrency.format(settlement.tdsAmount)}', color: Colors.red),
const Divider(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Net Bank Remittance Payout:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
Text(
formatCurrency.format(settlement.netPayoutAmount),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Color(0xFF10B981)),
),
],
),
],
),
),
const SizedBox(height: 16),
],
);
},
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
),
],
// Action Buttons Row: Settle Marketplace Payout / Record Payment & Process Return
Row(
children: [
if (latestInvoice.status != 'RETURNED') ...[
Expanded(
child: OutlinedButton.icon(
onPressed: () async {
final result = await showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (_) => CreateCreditNoteSheet(invoice: latestInvoice),
);
if (result != null) {
ref.refresh(invoicesProvider);
if (latestInvoice.id != null) {
ref.refresh(invoiceCreditNotesProvider(latestInvoice.id!));
}
}
},
icon: const Icon(LucideIcons.undo2, size: 16, color: Colors.red),
label: const Text('Return / Credit Note', style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold, fontSize: 13)),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: BorderSide(color: Colors.red.shade300),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
const SizedBox(width: 10),
],
if (remaining > 0) ...[
if (latestInvoice.salesChannel != null &&
latestInvoice.salesChannel!.toUpperCase() != 'DIRECT_POS' &&
latestInvoice.salesChannel!.toUpperCase() != 'STORE') ...[
Expanded(
child: ElevatedButton.icon(
onPressed: () async {
final result = await showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (_) => SettleMarketplaceOrderSheet(invoice: latestInvoice),
);
if (result != null) {
ref.refresh(invoicesProvider);
if (latestInvoice.id != null) {
ref.refresh(invoiceSettlementProvider(latestInvoice.id!));
}
}
},
icon: const Icon(LucideIcons.landmark, size: 16),
label: const Text('Settle Payout', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
backgroundColor: const Color(0xFF10B981),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
] else ...[
Expanded(
child: ElevatedButton.icon(
onPressed: () => _showReceivePaymentSheet(context, ref, latestInvoice),
icon: const Icon(LucideIcons.plusCircle, size: 16),
label: const Text('Record Payment', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
backgroundColor: Colors.green,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
],
],
],
),
const SizedBox(height: 30),
],
),
@@ -555,6 +782,23 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
);
}
Color _getDispatchColor(String? status) {
switch (status?.toUpperCase()) {
case 'DELIVERED':
return Colors.green;
case 'SHIPPED':
case 'IN_TRANSIT':
return Colors.indigo;
case 'PACKED':
return Colors.blue;
case 'RTO':
return Colors.red;
case 'PENDING':
default:
return Colors.grey;
}
}
Widget _buildDetailBadge(String text, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
@@ -656,7 +900,9 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
await Share.shareXFiles([XFile(imagePath.path)], text: 'Invoice $invoiceNumber');
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error sharing image: $e')));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error sharing image: $e')));
}
}
}
@@ -668,31 +914,28 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
orElse: () => widget.invoice,
) ?? widget.invoice;
final pdf = pw.Document();
final business = ref.read(businessProfileProvider).value;
final customers = ref.read(customersProvider).value ?? [];
final customer = customers.where((c) => c.id == latestInvoice.customerId).firstOrNull;
final business = ref.read(businessProfileProvider).value;
final products = ref.read(productsProvider).value ?? [];
final categories = ref.read(productCategoriesProvider).value ?? [];
final uoms = ref.read(uomsProvider).value ?? [];
final indianStates = ref.read(indianStatesProvider).value ?? [];
final businessState = indianStates.where((s) => s.id == business?.stateId).firstOrNull;
final customerState = indianStates.where((s) => s.id == (latestInvoice.placeOfSupplyStateId ?? customer?.stateId)).firstOrNull;
final isSameState = businessState != null && customerState != null && businessState.id == customerState.id;
final formatCurrency = NumberFormat.currency(symbol: 'Rs. ', decimalDigits: 2);
final formatDate = DateFormat('dd MMM yyyy');
final formatCurrency = NumberFormat.currency(symbol: 'Rs. ', decimalDigits: 2);
final font = await PdfGoogleFonts.robotoRegular();
final boldFont = await PdfGoogleFonts.robotoBold();
final businessStateId = business?.stateId;
final customerStateId = customer?.stateId;
final isSameState = businessStateId != null && customerStateId != null
? (businessStateId == customerStateId)
: true;
// Group items by product ID if available, or itemize
// Group items by productId
final Map<int, List<InvoiceItem>> groupedItems = {};
final List<InvoiceItem> ungroupedItems = [];
for (final item in latestInvoice.items) {
for (var item in latestInvoice.items) {
if (item.productId != null) {
groupedItems.putIfAbsent(item.productId!, () => []).add(item);
} else {
@@ -700,96 +943,64 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
}
}
final pdf = pw.Document();
pdf.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
theme: pw.ThemeData.withFont(
base: font,
bold: boldFont,
),
margin: const pw.EdgeInsets.all(28),
build: (pw.Context context) {
return [
// 1. Header with Business Details & TAX INVOICE title
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Expanded(
child: 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 && business!.address!.isNotEmpty)
margin: const pw.EdgeInsets.all(32),
build: (context) => [
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
// 1. Enterprise Invoice Header
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
business.address!,
style: const pw.TextStyle(fontSize: 9, color: PdfColors.grey700),
),
if (business?.contactNumber != null && business!.contactNumber!.isNotEmpty)
pw.Text(
'Phone: ${business.contactNumber}',
style: const pw.TextStyle(fontSize: 9, color: PdfColors.grey700),
),
if (business?.gstin != null && business!.gstin!.isNotEmpty)
pw.Text(
'GSTIN: ${business.gstin}',
business?.businessName ?? 'KIFI ENTERPRISE',
style: pw.TextStyle(
fontSize: 9,
fontSize: 18,
fontWeight: pw.FontWeight.bold,
color: PdfColors.blue800,
color: PdfColors.blue900,
),
),
],
if (business?.address != null && business!.address!.isNotEmpty)
pw.Text(
business.address!,
style: const pw.TextStyle(fontSize: 9, color: PdfColors.grey700),
),
if (business?.contactNumber != null && business!.contactNumber!.isNotEmpty)
pw.Text(
'Phone: ${business.contactNumber}',
style: const pw.TextStyle(fontSize: 9, color: PdfColors.grey700),
),
if (business?.gstin != null && business!.gstin!.isNotEmpty)
pw.Text(
'GSTIN: ${business.gstin}',
style: pw.TextStyle(
fontSize: 9,
fontWeight: pw.FontWeight.bold,
color: PdfColors.blue800,
),
),
],
),
),
),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text(
'TAX INVOICE',
style: pw.TextStyle(
fontSize: 22,
fontSize: 18,
fontWeight: pw.FontWeight.bold,
color: PdfColors.blue900,
),
),
pw.SizedBox(height: 6),
pw.Container(
padding: const pw.EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: pw.BoxDecoration(
color: _getEffectiveStatus(latestInvoice) == 'PAID'
? PdfColors.green100
: (_getEffectiveStatus(latestInvoice) == 'PARTIAL' ? PdfColors.orange100 : PdfColors.grey200),
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(4)),
border: pw.Border.all(
color: _getEffectiveStatus(latestInvoice) == 'PAID'
? PdfColors.green700
: (_getEffectiveStatus(latestInvoice) == 'PARTIAL' ? PdfColors.orange700 : PdfColors.grey500),
width: 0.5,
),
),
child: pw.Text(
_getEffectiveStatus(latestInvoice),
style: pw.TextStyle(
fontSize: 9,
fontWeight: pw.FontWeight.bold,
color: _getEffectiveStatus(latestInvoice) == 'PAID'
? PdfColors.green900
: (_getEffectiveStatus(latestInvoice) == 'PARTIAL' ? PdfColors.orange900 : PdfColors.grey800),
),
),
),
pw.SizedBox(height: 6),
pw.Text(
'Invoice #: ${latestInvoice.invoiceNumber}',
style: pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold),
@@ -798,6 +1009,21 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
'Date: ${formatDate.format(latestInvoice.issueDate)}',
style: const pw.TextStyle(fontSize: 9, color: PdfColors.grey700),
),
if (latestInvoice.salesChannel != null && latestInvoice.salesChannel!.isNotEmpty)
pw.Text(
'Channel: ${latestInvoice.salesChannel!}',
style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, color: PdfColors.blue900),
),
if (latestInvoice.marketplaceOrderId != null && latestInvoice.marketplaceOrderId!.isNotEmpty)
pw.Text(
'Order Ref: #${latestInvoice.marketplaceOrderId!}',
style: const pw.TextStyle(fontSize: 8.5, color: PdfColors.grey700),
),
if (latestInvoice.courierPartner != null || latestInvoice.trackingNumber != null)
pw.Text(
'Courier: ${latestInvoice.courierPartner ?? ""}${latestInvoice.trackingNumber != null ? " (AWB: ${latestInvoice.trackingNumber})" : ""}',
style: const pw.TextStyle(fontSize: 8.5, color: PdfColors.grey700),
),
if (latestInvoice.dueDate != null)
pw.Text(
'Due Date: ${formatDate.format(latestInvoice.dueDate!)}',
@@ -809,7 +1035,7 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
),
pw.SizedBox(height: 18),
// 2. Bill To Box (Customer Details)
// 2. Bill To & Ship To Box (Customer & Delivery Details)
pw.Container(
padding: const pw.EdgeInsets.all(10),
decoration: pw.BoxDecoration(
@@ -836,7 +1062,7 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
pw.Text(
customer?.name ?? 'Walk-in Customer',
style: pw.TextStyle(
fontSize: 12,
fontSize: 11,
fontWeight: pw.FontWeight.bold,
),
),
@@ -852,6 +1078,38 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
],
),
),
if (latestInvoice.shippingAddress != null && latestInvoice.shippingAddress!.isNotEmpty) ...[
pw.SizedBox(width: 14),
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
'SHIP TO (DELIVERY):',
style: pw.TextStyle(
fontSize: 8.5,
fontWeight: pw.FontWeight.bold,
color: PdfColors.grey700,
),
),
pw.SizedBox(height: 2),
pw.Text(
customer?.name ?? 'Recipient',
style: pw.TextStyle(
fontSize: 11,
fontWeight: pw.FontWeight.bold,
),
),
pw.Text(
latestInvoice.shippingAddress! + (latestInvoice.shippingPincode != null ? ' - ${latestInvoice.shippingPincode}' : ''),
style: const pw.TextStyle(fontSize: 9),
),
if (latestInvoice.courierPartner != null)
pw.Text('Via: ${latestInvoice.courierPartner}', style: const pw.TextStyle(fontSize: 8.5, color: PdfColors.grey700)),
],
),
),
],
],
),
),
@@ -1421,10 +1679,11 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
),
],
),
];
},
),
);
],
),
],
),
);
final pdfBytes = await pdf.save();