import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:image_picker/image_picker.dart'; import 'dart:io'; import 'package:dio/dio.dart'; import '../../../core/network/dio_client.dart'; import 'package:intl/intl.dart'; import '../../../core/widgets/premium_text_field.dart'; import '../../../core/widgets/smart_search_dropdown.dart'; import '../../../core/widgets/barcode_scanner_screen.dart'; import '../../inventory/providers/products_provider.dart'; import '../../inventory/providers/product_categories_provider.dart'; import '../../inventory/providers/commodity_rates_provider.dart'; import '../../inventory/providers/inventory_items_provider.dart'; import '../../inventory/providers/uoms_provider.dart'; import '../../inventory/domain/product.dart'; import '../../inventory/domain/inventory_item.dart'; import '../providers/customers_provider.dart'; import '../providers/invoices_provider.dart'; import '../domain/customer.dart'; import '../domain/invoice.dart'; import '../providers/sales_channels_provider.dart'; import '../../business/providers/business_provider.dart'; import '../../business/providers/indian_states_provider.dart'; import '../../transactions/providers/providers.dart'; import '../../transactions/presentation/widgets/attachment_gallery_screen.dart'; import 'widgets/quick_add_customer_sheet.dart'; import '../../../core/utils/purity_utils.dart'; import 'invoice_details_screen.dart'; bool _isCommodityProduct({ ProductCategory? category, Product? product, InventoryItem? inventoryItem, InvoiceItem? invoiceItem, String? commodityCode, String? huid, }) { final h = huid ?? invoiceItem?.huid ?? inventoryItem?.huid; if (h != null && h.trim().isNotEmpty) return true; final code = commodityCode ?? category?.commodityCode ?? invoiceItem?.commodityCode; if (code != null && code.trim().isNotEmpty && code.trim().toUpperCase() != 'NONE') { return true; } return false; } String _resolveProductUnit({ required Product? product, required ProductCategory? category, required List uoms, bool isCommodity = false, }) { if (isCommodity) { return (category != null && category.baseUnit.isNotEmpty) ? category.baseUnit : 'g'; } if (product?.uomId != null) { final uom = uoms.where((u) => u.id == product!.uomId).firstOrNull; if (uom != null && uom.abbreviation != null && uom.abbreviation!.isNotEmpty) return uom.abbreviation!; if (uom != null && uom.name.isNotEmpty) return uom.name; } if (category != null && category.baseUnit.isNotEmpty) { return category.baseUnit; } return 'pcs'; } 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 InvoiceBuilderScreen extends ConsumerStatefulWidget { final Invoice? existingInvoice; final int? initialCustomerId; final List? initialItems; const InvoiceBuilderScreen({ super.key, this.existingInvoice, this.initialCustomerId, this.initialItems, }); @override ConsumerState createState() => _InvoiceBuilderScreenState(); } class _InvoiceBuilderScreenState extends ConsumerState { final _formKey = GlobalKey(); final _invoiceNumberCtrl = TextEditingController(); final _notesCtrl = TextEditingController(); final _amountPaidCtrl = TextEditingController(); final _emiAmountCtrl = TextEditingController(); final _discountCtrl = TextEditingController(text: '0.0'); final _marketplaceOrderIdCtrl = TextEditingController(); final _courierPartnerCtrl = TextEditingController(); final _trackingNumberCtrl = TextEditingController(); final _shippingAddressCtrl = TextEditingController(); final _shippingPincodeCtrl = TextEditingController(); DateTime _invoiceDate = DateTime.now(); DateTime? _dueDate; int? _selectedCustomerId; int? _selectedSalesChannelId; String? _selectedSalesChannelName; int? _selectedPlaceOfSupplyStateId; String? _selectedPlaceOfSupplyStateName; final List _items = []; bool _isLoading = false; // Logistics & Delivery State String _dispatchStatus = 'PENDING'; bool _sameAsCustomerAddress = true; DateTime? _shippedAt; XFile? _invoiceFile; String? _invoiceUrl; String _paymentMethod = 'Cash'; int? _selectedWalletId; DateTime? _nextPaymentDate; bool _isAmountPaidEdited = false; // EMI State bool _isEmi = false; String _emiCycle = 'MONTHLY'; DateTime? _emiStartDate; @override void initState() { super.initState(); if (widget.existingInvoice != null) { final inv = widget.existingInvoice!; _invoiceNumberCtrl.text = inv.invoiceNumber; _invoiceDate = inv.issueDate; _dueDate = inv.dueDate; _selectedCustomerId = inv.customerId; _selectedSalesChannelId = inv.salesChannelId; _selectedSalesChannelName = inv.salesChannel; _marketplaceOrderIdCtrl.text = inv.marketplaceOrderId ?? ''; _courierPartnerCtrl.text = inv.courierPartner ?? ''; _trackingNumberCtrl.text = inv.trackingNumber ?? ''; _dispatchStatus = inv.dispatchStatus ?? 'PENDING'; _shippedAt = inv.shippedAt; _shippingAddressCtrl.text = inv.shippingAddress ?? ''; _shippingPincodeCtrl.text = inv.shippingPincode ?? ''; _sameAsCustomerAddress = (inv.shippingAddress == null || inv.shippingAddress!.trim().isEmpty); _selectedPlaceOfSupplyStateId = inv.placeOfSupplyStateId; _selectedPlaceOfSupplyStateName = inv.placeOfSupply; _notesCtrl.text = inv.notes ?? ''; _items.addAll(inv.items); _invoiceUrl = inv.invoiceUrl; if (inv.discountTotal > 0) { _discountCtrl.text = inv.discountTotal.toStringAsFixed(2); } _amountPaidCtrl.text = inv.amountPaid > 0 ? inv.amountPaid.toStringAsFixed(2) : ''; _selectedWalletId = inv.paymentWalletId; _paymentMethod = inv.paymentMethod ?? 'Cash'; _nextPaymentDate = inv.nextPaymentDate; _isAmountPaidEdited = inv.amountPaid > 0; _isEmi = inv.isEmi; _emiCycle = inv.emiCycle ?? 'MONTHLY'; _emiStartDate = inv.emiStartDate; if (inv.emiAmount != null) { _emiAmountCtrl.text = inv.emiAmount!.toStringAsFixed(2); } } else { _invoiceNumberCtrl.text = 'INV-${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}'; _selectedCustomerId = widget.initialCustomerId; _dueDate = DateTime.now().add(const Duration(days: 30)); _emiStartDate = DateTime.now().add(const Duration(days: 30)); if (widget.initialItems != null) { _items.addAll(widget.initialItems!); } } } @override void dispose() { _invoiceNumberCtrl.dispose(); _notesCtrl.dispose(); _amountPaidCtrl.dispose(); _emiAmountCtrl.dispose(); _discountCtrl.dispose(); _marketplaceOrderIdCtrl.dispose(); _courierPartnerCtrl.dispose(); _trackingNumberCtrl.dispose(); _shippingAddressCtrl.dispose(); _shippingPincodeCtrl.dispose(); super.dispose(); } Future _pickInvoiceFile() async { try { final picker = ImagePicker(); final picked = await picker.pickImage( source: ImageSource.gallery, imageQuality: 80, ); if (picked != null) { setState(() { _invoiceFile = picked; _isLoading = true; }); final bytes = await picked.readAsBytes(); final formData = FormData.fromMap({ 'file': MultipartFile.fromBytes( bytes, filename: picked.name.isNotEmpty ? picked.name : 'invoice_${DateTime.now().millisecondsSinceEpoch}.jpg', ), 'type': 'SALES_INVOICE', }); final uploadResp = await DioClient().dio.post( '/upload', data: formData, ); if (uploadResp.statusCode == 200) { setState(() { _invoiceUrl = uploadResp.data['url']; }); } } } catch (e) { debugPrint('Error uploading sales invoice attachment: $e'); } finally { if (mounted) setState(() => _isLoading = false); } } void _showQuickAddCustomer() { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => const QuickAddCustomerSheet(), ).then((result) { if (result != null) { if (result is Customer && result.id != null) { setState(() => _selectedCustomerId = result.id); } else { ref.refresh(customersProvider); } } }); } void _openAddItemSheet({int? editIndex}) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => _AddSalesItemSheet( initialItem: editIndex != null ? _items[editIndex] : null, onItemAdded: (item) { setState(() { if (editIndex != null) { _items[editIndex] = item; } else { _items.add(item); } }); }, ), ); } Future _scanBarcode() async { final scannedCode = await Navigator.push( context, MaterialPageRoute(builder: (_) => const BarcodeScannerScreen()), ); if (scannedCode != null && scannedCode.isNotEmpty) { final inventoryItems = ref.read(inventoryItemsProvider).value ?? []; final products = ref.read(productsProvider).value ?? []; final categories = ref.read(productCategoriesProvider).value ?? []; final commodityRates = ref.read(commodityRatesProvider).value ?? []; // Check inventory item by HUID or SKU or Barcode final invItem = inventoryItems.where((i) => (i.huid != null && i.huid!.toUpperCase() == scannedCode.toUpperCase()) || (i.sku != null && i.sku!.toUpperCase() == scannedCode.toUpperCase()) ).firstOrNull; Product? matchedProduct; if (invItem != null) { matchedProduct = products.where((p) => p.id == invItem.productId).firstOrNull; } else { matchedProduct = products.where((p) => (p.barcode != null && p.barcode == scannedCode) || (p.sku != null && p.sku!.toUpperCase() == scannedCode.toUpperCase()) ).firstOrNull; } if (matchedProduct != null) { final cat = categories.where((c) => c.id == matchedProduct!.categoryId).firstOrNull; double commodityRate = 0.0; if (cat?.commodityCode != null) { final match = commodityRates.where((r) => r.commodityCode.toUpperCase() == cat!.commodityCode!.toUpperCase()).firstOrNull; if (match != null) commodityRate = match.rate; } final purity = resolvePurity(categoryPurity: cat?.purityFactor, productPurity: matchedProduct.purityFactor); final effectiveRate = commodityRate > 0 ? (commodityRate * purity) : (matchedProduct.sellingPrice ?? 0.0); final weight = invItem?.grossWeight ?? 1.0; final makingCharge = invItem?.makingCharges ?? matchedProduct.makingCharges ?? cat?.defaultMakingCharge ?? 0.0; final makingType = invItem?.makingChargeType ?? matchedProduct.makingChargesType ?? cat?.makingChargeType ?? 'PER_GRAM'; final metalAmount = weight * effectiveRate; double makingAmt = 0.0; if (makingType == 'PERCENTAGE') { makingAmt = metalAmount * (makingCharge / 100.0); } else if (makingType == 'PER_PIECE') { makingAmt = makingCharge; } else { makingAmt = weight * makingCharge; } final gstRate = matchedProduct.gstRate ?? (cat?.defaultGst ?? 3.0); final taxable = metalAmount + makingAmt; final taxAmount = (taxable * gstRate) / 100.0; final total = taxable + taxAmount; final item = InvoiceItem( productId: matchedProduct.id, inventoryItemId: invItem?.id, productName: matchedProduct.name, categoryName: cat?.name, commodityCode: cat?.commodityCode, hsnCode: cat?.defaultHsn ?? matchedProduct.hsnCode, sku: invItem?.sku ?? matchedProduct.sku, huid: invItem?.huid, description: matchedProduct.name, quantity: 1.0, weight: weight, unitPrice: effectiveRate, makingCharge: makingCharge, makingChargesType: makingType, taxRate: gstRate, total: total, ); setState(() => _items.add(item)); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Added ${matchedProduct.name} via barcode scan!'), backgroundColor: Colors.green), ); } } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('No item found with barcode/HUID: $scannedCode'), backgroundColor: Colors.orange), ); } } } } Future _saveInvoice() async { if (!_formKey.currentState!.validate()) return; if (_items.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Please add at least one line item')), ); return; } setState(() => _isLoading = true); try { String? finalInvoiceUrl = _invoiceUrl; if (_invoiceFile != null && finalInvoiceUrl == null) { final bytes = await _invoiceFile!.readAsBytes(); final formData = FormData.fromMap({ 'file': MultipartFile.fromBytes( bytes, filename: _invoiceFile!.name.isNotEmpty ? _invoiceFile!.name : 'invoice.jpg', ), 'type': 'SALES_INVOICE', }); final uploadResp = await DioClient().dio.post( '/upload', data: formData, ); if (uploadResp.statusCode == 200) { finalInvoiceUrl = uploadResp.data['url']; } } final products = ref.read(productsProvider).value ?? []; final categories = ref.read(productCategoriesProvider).value ?? []; final businessStateId = ref.read(businessProfileProvider).value?.stateId; final customers = ref.read(customersProvider).value ?? []; final customer = _selectedCustomerId == null ? null : customers.where((c) => c.id == _selectedCustomerId).firstOrNull; final effectiveSupplyStateId = _selectedPlaceOfSupplyStateId ?? customer?.stateId ?? businessStateId; final isSameState = businessStateId != null && effectiveSupplyStateId != null && businessStateId == effectiveSupplyStateId; double subtotal = 0; double totalCgst = 0; double totalSgst = 0; double totalIgst = 0; double totalTax = 0; double totalMaking = 0; double totalDiscount = 0; double grandTotal = 0; final List processedItems = []; for (var item in _items) { final product = products.where((p) => p.id == item.productId).firstOrNull; final category = categories.where((c) => c.name == item.categoryName || c.id == product?.categoryId).firstOrNull; final gstRate = item.taxRate > 0 ? item.taxRate : (product?.gstRate ?? 0.0); final qtyOrWeight = item.weight != null && item.weight! > 0 ? item.weight! : (item.quantity > 0 ? item.quantity : 1.0); final baseItemAmt = qtyOrWeight * item.unitPrice; final isItemCommodity = _isCommodityProduct(category: category, product: product, invoiceItem: item); double makingChargeAmt = 0.0; if (isItemCommodity) { if (item.makingChargesType == 'PERCENTAGE') { makingChargeAmt = baseItemAmt * (item.makingCharge / 100.0); } else if (item.makingChargesType == 'PER_PIECE') { makingChargeAmt = item.makingCharge; } else { makingChargeAmt = qtyOrWeight * item.makingCharge; } } final taxableAmount = baseItemAmt + makingChargeAmt + item.otherCharges - item.discount; final taxAmount = (taxableAmount * gstRate) / 100.0; double cgst = 0; double sgst = 0; double igst = 0; if (isSameState) { cgst = taxAmount / 2.0; sgst = taxAmount / 2.0; totalCgst += cgst; totalSgst += sgst; } else { igst = taxAmount; totalIgst += igst; } final lineTotal = taxableAmount + taxAmount; subtotal += baseItemAmt; totalMaking += makingChargeAmt; totalTax += taxAmount; processedItems.add(item.copyWith( quantity: item.quantity > 0 ? item.quantity : qtyOrWeight, weight: item.weight, taxRate: gstRate, cgst: cgst, sgst: sgst, igst: igst, total: lineTotal, )); } final invoiceDiscount = double.tryParse(_discountCtrl.text) ?? 0.0; totalDiscount = double.parse(invoiceDiscount.toStringAsFixed(2)); final rawGrandTotal = (subtotal + totalMaking - invoiceDiscount).clamp(0.0, double.infinity) + totalTax; grandTotal = double.parse(rawGrandTotal.toStringAsFixed(2)); final amountPaid = double.tryParse(_amountPaidCtrl.text) ?? (_isAmountPaidEdited ? 0.0 : grandTotal); final validAmountPaid = double.parse((amountPaid > grandTotal ? grandTotal : amountPaid).toStringAsFixed(2)); final balanceDue = double.parse((grandTotal - validAmountPaid).clamp(0.0, double.infinity).toStringAsFixed(2)); final isFullyPaid = balanceDue.abs() < 0.01 || validAmountPaid >= grandTotal - 0.01; final determinedStatus = isFullyPaid ? 'PAID' : (validAmountPaid > 0.01 ? 'PARTIAL' : 'DRAFT'); final invoice = Invoice( id: widget.existingInvoice?.id, customerId: _selectedCustomerId, invoiceNumber: _invoiceNumberCtrl.text.trim(), issueDate: _invoiceDate, dueDate: _dueDate, subtotal: double.parse(subtotal.toStringAsFixed(2)), taxTotal: double.parse(totalTax.toStringAsFixed(2)), cgstTotal: double.parse(totalCgst.toStringAsFixed(2)), sgstTotal: double.parse(totalSgst.toStringAsFixed(2)), igstTotal: double.parse(totalIgst.toStringAsFixed(2)), discountTotal: totalDiscount, totalAmount: grandTotal, amountPaid: validAmountPaid, paymentMethod: validAmountPaid > 0 ? _paymentMethod : null, paymentWalletId: validAmountPaid > 0 ? (_selectedWalletId ?? ref.read(walletProvider).value?.where((w) => w.nature == 'CASH' || w.nature == 'SAVINGS').firstOrNull?.id) : null, nextPaymentDate: balanceDue > 0.01 ? (_nextPaymentDate ?? DateTime.now().add(const Duration(days: 30))) : null, status: widget.existingInvoice != null ? (isFullyPaid ? 'PAID' : (validAmountPaid > 0.01 ? 'PARTIAL' : widget.existingInvoice!.status)) : determinedStatus, notes: _notesCtrl.text.trim().isEmpty ? null : _notesCtrl.text.trim(), invoiceUrl: finalInvoiceUrl, isEmi: _isEmi, emiAmount: _isEmi && _emiAmountCtrl.text.isNotEmpty ? double.tryParse(_emiAmountCtrl.text) : null, emiCycle: _isEmi ? _emiCycle : null, emiStartDate: _isEmi ? (_emiStartDate ?? DateTime.now().add(const Duration(days: 30))) : null, salesChannelId: _selectedSalesChannelId, salesChannel: _selectedSalesChannelName, marketplaceOrderId: _marketplaceOrderIdCtrl.text.trim().isNotEmpty ? _marketplaceOrderIdCtrl.text.trim() : null, courierPartner: _courierPartnerCtrl.text.trim().isNotEmpty ? _courierPartnerCtrl.text.trim() : null, trackingNumber: _trackingNumberCtrl.text.trim().isNotEmpty ? _trackingNumberCtrl.text.trim() : null, dispatchStatus: _dispatchStatus, shippedAt: _shippedAt ?? (_dispatchStatus == 'SHIPPED' || _dispatchStatus == 'IN_TRANSIT' || _dispatchStatus == 'DELIVERED' ? DateTime.now() : null), shippingAddress: !_sameAsCustomerAddress && _shippingAddressCtrl.text.trim().isNotEmpty ? _shippingAddressCtrl.text.trim() : null, shippingPincode: !_sameAsCustomerAddress && _shippingPincodeCtrl.text.trim().isNotEmpty ? _shippingPincodeCtrl.text.trim() : null, placeOfSupplyStateId: effectiveSupplyStateId, placeOfSupply: _selectedPlaceOfSupplyStateName ?? ref.read(indianStatesProvider).value?.where((s) => s.id == effectiveSupplyStateId).firstOrNull?.name, items: processedItems, ); Invoice? savedInvoice; if (widget.existingInvoice == null) { savedInvoice = await ref.read(invoicesProvider.notifier).createInvoice(invoice); } else { final resp = await DioClient().dio.put( '/invoices/${widget.existingInvoice!.id}', data: invoice.toJson(), ); if (resp.statusCode == 200) { savedInvoice = Invoice.fromJson(resp.data); ref.refresh(invoicesProvider); } } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Sales Invoice saved successfully!'), backgroundColor: Colors.green), ); if (savedInvoice != null) { Navigator.pushReplacement( context, MaterialPageRoute( builder: (_) => InvoiceDetailsScreen(invoice: savedInvoice!), ), ); } else { Navigator.pop(context); } } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error saving invoice: $e'), backgroundColor: Colors.red), ); } } finally { if (mounted) setState(() => _isLoading = false); } } @override Widget build(BuildContext context) { final customersState = ref.watch(customersProvider); final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( appBar: AppBar( title: Text( widget.existingInvoice == null ? 'New Sales Invoice' : 'Edit Invoice ${_invoiceNumberCtrl.text}', style: const TextStyle(fontWeight: FontWeight.bold), ), actions: [ IconButton( icon: const Icon(LucideIcons.qrCode), tooltip: 'Scan Barcode/HUID', onPressed: _scanBarcode, ), if (_isLoading) const Padding( padding: EdgeInsets.all(16.0), child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ) else TextButton.icon( onPressed: _saveInvoice, icon: const Icon(LucideIcons.check, size: 18), label: const Text('Save', style: TextStyle(fontWeight: FontWeight.bold)), ), ], ), body: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), behavior: HitTestBehavior.translucent, child: Form( key: _formKey, child: ListView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, padding: const EdgeInsets.all(16.0), children: [ // E-Commerce / Sales Channels Selector (If enabled) Builder( builder: (context) { final featureState = ref.watch(businessFeatureProvider); final businessProfile = ref.watch(businessProfileProvider).value; final nature = (businessProfile?.natureOfBusiness ?? businessProfile?.industry ?? 'JEWELLERY').toUpperCase(); final isJewellery = nature == 'JEWELLERY'; final isChannelsEnabled = featureState.value?.enableEcommerceChannels ?? (!isJewellery); if (!isChannelsEnabled) return const SizedBox.shrink(); final salesChannelsState = ref.watch(salesChannelsProvider); return salesChannelsState.when( data: (channels) { final activeChannels = channels.where((c) => c.isActive).toList(); if (activeChannels.isEmpty) return const SizedBox.shrink(); final currentSelectedId = _selectedSalesChannelId; final selectedChannel = activeChannels.where((c) => (currentSelectedId != null && c.id == currentSelectedId) || (currentSelectedId == null && c.name.toUpperCase() == (_selectedSalesChannelName ?? '').toUpperCase()) ).firstOrNull ?? activeChannels.where((c) => c.code == 'DIRECT' || c.name.toLowerCase().contains('direct')).firstOrNull ?? activeChannels.first; final isMarketplace = (selectedChannel.code != 'DIRECT' && !selectedChannel.name.toLowerCase().contains('direct') && !selectedChannel.name.toLowerCase().contains('store')); return Container( margin: const EdgeInsets.only(bottom: 16.0), padding: const EdgeInsets.all(12.0), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.35), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.withValues(alpha: 0.2)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(LucideIcons.shoppingBag, size: 16, color: Colors.blue.shade700), const SizedBox(width: 6), const Text( 'Sales Channel / Marketplace', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13), ), ], ), const SizedBox(height: 10), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: activeChannels.map((channel) { final isSelected = (currentSelectedId != null && currentSelectedId == channel.id) || (currentSelectedId == null && selectedChannel.id == channel.id); IconData iconData = LucideIcons.store; if (channel.code == 'AMAZON' || channel.name.toLowerCase().contains('amazon')) { iconData = LucideIcons.shoppingCart; } else if (channel.code == 'FLIPKART' || channel.name.toLowerCase().contains('flipkart')) { iconData = LucideIcons.package; } else if (channel.code == 'SHOPIFY' || channel.name.toLowerCase().contains('shopify')) { iconData = LucideIcons.globe; } else if (channel.code == 'MEESHO' || channel.name.toLowerCase().contains('meesho')) { iconData = LucideIcons.tag; } else if (channel.code == 'QUICK_COMMERCE' || channel.name.toLowerCase().contains('quick')) { iconData = LucideIcons.zap; } return Padding( padding: const EdgeInsets.only(right: 8.0), child: ChoiceChip( selected: isSelected, avatar: Icon( iconData, size: 15, color: isSelected ? Colors.white : Colors.grey.shade700, ), label: Text( channel.name, style: TextStyle( fontSize: 12, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected ? Colors.white : Colors.grey.shade800, ), ), selectedColor: Colors.blue.shade700, onSelected: (selected) { if (selected) { setState(() { _selectedSalesChannelId = channel.id; _selectedSalesChannelName = channel.name; // If customer is not yet selected, attempt matching default channel customer (e.g. Amazon) if (_selectedCustomerId == null) { final customersList = ref.read(customersProvider).value ?? []; final match = customersList.where((c) => c.name.toLowerCase().contains(channel.name.toLowerCase()) || (channel.code != null && c.name.toLowerCase().contains(channel.code!.toLowerCase())) ).firstOrNull; if (match != null) { _selectedCustomerId = match.id; if (match.stateId != null) { _selectedPlaceOfSupplyStateId = match.stateId; } } } }); } }, ), ); }).toList(), ), ), if (isMarketplace) ...[ const SizedBox(height: 12), PremiumTextField( controller: _marketplaceOrderIdCtrl, labelText: '${selectedChannel.name} Order ID (e.g. 408-1234567-8901234)', prefixIcon: const Icon(LucideIcons.hash, size: 16), ), ], ], ), ); }, loading: () => const SizedBox.shrink(), error: (err, stack) => const SizedBox.shrink(), ); }, ), // Customer Selection Row + Quick Add Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: customersState.when( data: (customers) => SmartSearchDropdown( labelText: 'Customer *', hintText: 'Search Customer by Name / Phone / GSTIN / City', value: customers.where((c) => c.id == _selectedCustomerId).firstOrNull, items: customers, itemAsString: (c) => c.name, filterFn: (c, query) { final q = query.toLowerCase(); return c.name.toLowerCase().contains(q) || (c.phone != null && c.phone!.contains(q)) || (c.email != null && c.email!.toLowerCase().contains(q)) || (c.gstin != null && c.gstin!.toLowerCase().contains(q)) || (c.address != null && c.address!.toLowerCase().contains(q)); }, itemBuilder: (context, c) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ CircleAvatar( radius: 18, backgroundColor: Colors.blue.withValues(alpha: 0.15), child: Text( c.name.isNotEmpty ? c.name[0].toUpperCase() : 'C', style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.blue, fontSize: 14), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( c.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), const SizedBox(height: 3), Wrap( spacing: 6, runSpacing: 3, children: [ if (c.phone != null && c.phone!.isNotEmpty) _buildSmallBadge(c.phone!, Colors.teal), if (c.address != null && c.address!.isNotEmpty) _buildSmallBadge(c.address!, Colors.purple), if (c.gstin != null && c.gstin!.isNotEmpty) _buildSmallBadge('GST: ${c.gstin}', Colors.indigo), ], ), ], ), ), ], ), ); }, onChanged: (val) { setState(() { _selectedCustomerId = val?.id; if (val?.stateId != null) { _selectedPlaceOfSupplyStateId = val!.stateId; } }); }, ), loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Text('Error loading customers: $e'), ), ), const SizedBox(width: 8), Padding( padding: const EdgeInsets.only(top: 4.0), child: IconButton.filledTonal( onPressed: _showQuickAddCustomer, icon: const Icon(LucideIcons.userPlus, size: 20), tooltip: 'Quick Add Customer', ), ), ], ), const SizedBox(height: 12), // Place of Supply / Destination Delivery State Consumer( builder: (context, ref, child) { final statesState = ref.watch(indianStatesProvider); final businessStateId = ref.watch(businessProfileProvider).value?.stateId; final customers = ref.watch(customersProvider).value ?? []; final customer = _selectedCustomerId == null ? null : customers.where((c) => c.id == _selectedCustomerId).firstOrNull; final effectiveStateId = _selectedPlaceOfSupplyStateId ?? customer?.stateId ?? businessStateId; final isSameState = businessStateId != null && effectiveStateId != null && businessStateId == effectiveStateId; return statesState.when( data: (states) { final selectedState = states.where((s) => s.id == effectiveStateId).firstOrNull; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: isSameState ? Colors.green.withValues(alpha: 0.05) : Colors.purple.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(12), border: Border.all( color: isSameState ? Colors.green.withValues(alpha: 0.25) : Colors.purple.withValues(alpha: 0.25), ), ), child: Row( children: [ Icon( LucideIcons.mapPin, size: 18, color: isSameState ? Colors.green.shade700 : Colors.purple.shade700, ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'Place of Supply (Delivery State)', style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: Colors.grey.shade600, ), ), const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1.5), decoration: BoxDecoration( color: isSameState ? Colors.green.shade100 : Colors.purple.shade100, borderRadius: BorderRadius.circular(4), ), child: Text( isSameState ? 'Intra-state (CGST + SGST)' : 'Inter-state (IGST)', style: TextStyle( fontSize: 9.5, fontWeight: FontWeight.bold, color: isSameState ? Colors.green.shade900 : Colors.purple.shade900, ), ), ), ], ), const SizedBox(height: 2), DropdownButtonHideUnderline( child: DropdownButton( value: selectedState?.id, isDense: true, isExpanded: true, hint: const Text('Select State of Supply', style: TextStyle(fontSize: 13)), items: states.map((s) { final isHome = s.id == businessStateId; return DropdownMenuItem( value: s.id, child: Text( '${s.name} (${s.gstCode})${isHome ? " - Home State" : ""}', style: TextStyle( fontSize: 13, fontWeight: isHome ? FontWeight.bold : FontWeight.normal, ), ), ); }).toList(), onChanged: (val) { if (val != null) { final picked = states.where((s) => s.id == val).firstOrNull; setState(() { _selectedPlaceOfSupplyStateId = val; _selectedPlaceOfSupplyStateName = picked?.name; }); } }, ), ), ], ), ), ], ), ); }, loading: () => const SizedBox.shrink(), error: (err, stack) => const SizedBox.shrink(), ); }, ), const SizedBox(height: 16), PremiumTextField( controller: _invoiceNumberCtrl, labelText: 'Invoice Number *', prefixIcon: const Icon(LucideIcons.fileText), validator: (val) => val == null || val.isEmpty ? 'Required' : null, ), const SizedBox(height: 16), InkWell( onTap: () async { final picked = await showDatePicker( context: context, initialDate: _invoiceDate, firstDate: DateTime(2000), lastDate: DateTime(2100), ); if (picked != null) { setState(() => _invoiceDate = picked); } }, child: InputDecorator( decoration: InputDecoration( labelText: 'Invoice Date', prefixIcon: const Icon(LucideIcons.calendar), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), child: Text( DateFormat('dd MMM yyyy').format(_invoiceDate), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14), ), ), ), const SizedBox(height: 24), // Items Section Header with Barcode + Add Item Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Invoice Items (${_items.length})', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), Row( children: [ IconButton.filledTonal( onPressed: _scanBarcode, icon: const Icon(LucideIcons.qrCode, size: 18), tooltip: 'Scan Barcode', ), const SizedBox(width: 8), FilledButton.tonalIcon( onPressed: () => _openAddItemSheet(), icon: const Icon(LucideIcons.plus, size: 16), label: const Text('Add Item'), ), ], ), ], ), const SizedBox(height: 12), // Items List if (_items.isEmpty) Container( width: double.infinity, padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: isDark ? const Color(0xFF1E293B) : Colors.grey.shade100, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.withValues(alpha: 0.2)), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(LucideIcons.packageOpen, size: 48, color: Colors.grey.shade400), const SizedBox(height: 12), Text( 'No items added yet', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.grey.shade600), ), const SizedBox(height: 4), Text( 'Tap "Add Item" or scan a barcode/SKU to add sales items', style: TextStyle(fontSize: 12, color: Colors.grey.shade500), ), ], ), ) else ..._items.asMap().entries.map((entry) { final idx = entry.key; final item = entry.value; return _buildSalesItemCard(item, idx, isDark); }), const SizedBox(height: 20), // Invoice Discount Input Field before Summary PremiumTextField( controller: _discountCtrl, labelText: 'Invoice Discount (₹)', prefixIcon: const Icon(LucideIcons.tag, color: Colors.red), keyboardType: const TextInputType.numberWithOptions(decimal: true), onChanged: (_) => setState(() {}), ), const SizedBox(height: 20), // Grand Total & Tax Summary Box _buildTotalsSummaryCard(isDark), const SizedBox(height: 24), // Payment / Settlement Details (Full Partial Payment & EMI Support) _buildPaymentSection(isDark), const SizedBox(height: 24), // Shipping & Courier Logistics Section _buildShippingLogisticsSection(isDark), const SizedBox(height: 24), // Notes & Remarks PremiumTextField( controller: _notesCtrl, labelText: 'Notes & Terms', maxLines: 3, prefixIcon: const Icon(LucideIcons.alignLeft), ), const SizedBox(height: 24), // Attachments Text( 'Attached Invoice / Documents', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), if (_invoiceFile != null) ListTile( leading: const Icon(LucideIcons.image, color: Colors.blue), title: Text(_invoiceFile!.name), subtitle: const Text('Tap to view image', style: TextStyle(fontSize: 12, color: Colors.blue)), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (_) => AttachmentGalleryScreen( images: [kIsWeb ? NetworkImage(_invoiceFile!.path) : FileImage(File(_invoiceFile!.path))], initialIndex: 0, ), ), ); }, trailing: IconButton( icon: const Icon(LucideIcons.x), onPressed: () => setState(() => _invoiceFile = null), ), ) else if (_invoiceUrl != null && _invoiceUrl!.isNotEmpty) ListTile( leading: const Icon(LucideIcons.image, color: Colors.blue), title: const Text('View Attached Invoice'), subtitle: const Text('Tap to view full image', style: TextStyle(fontSize: 12, color: Colors.blue)), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (_) => AttachmentGalleryScreen( images: [NetworkImage(_resolveImageUrl(_invoiceUrl!))], initialIndex: 0, ), ), ); }, trailing: IconButton( icon: const Icon(LucideIcons.x), onPressed: () => setState(() => _invoiceUrl = null), ), ) else OutlinedButton.icon( onPressed: _pickInvoiceFile, icon: const Icon(LucideIcons.paperclip), label: const Text('Attach Sales Bill / Slip'), style: OutlinedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 14), ), ), const SizedBox(height: 40), ], ), ), ), ); } Widget _buildSalesItemCard(InvoiceItem item, int index, bool isDark) { final uoms = ref.watch(uomsProvider).value ?? []; final products = ref.watch(productsProvider).value ?? []; final categories = ref.watch(productCategoriesProvider).value ?? []; final product = products.where((p) => p.id == item.productId).firstOrNull; final category = categories.where((c) => c.name == item.categoryName || c.id == product?.categoryId).firstOrNull; final isItemCommodity = _isCommodityProduct( category: category, product: product, invoiceItem: item, ); final qtyOrWeight = item.weight != null && item.weight! > 0 ? item.weight! : (item.quantity > 0 ? item.quantity : 1.0); final baseAmount = qtyOrWeight * item.unitPrice; double makingChargeAmt = 0.0; if (isItemCommodity) { if (item.makingChargesType == 'PERCENTAGE') { makingChargeAmt = baseAmount * (item.makingCharge / 100.0); } else if (item.makingChargesType == 'PER_PIECE') { makingChargeAmt = item.makingCharge; } else { makingChargeAmt = qtyOrWeight * item.makingCharge; } } final taxableAmount = baseAmount + makingChargeAmt + item.otherCharges - item.discount; final taxAmount = (taxableAmount * item.taxRate) / 100.0; final calculatedTotal = item.total > 0 ? item.total : (taxableAmount + taxAmount); final unit = _resolveProductUnit(product: product, category: category, uoms: uoms, isCommodity: isItemCommodity); 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), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.04), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (item.photoUrl != null && item.photoUrl!.isNotEmpty) Container( width: 48, height: 48, 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 ?? item.description ?? 'Item #${index + 1}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), const SizedBox(height: 4), Wrap( spacing: 6, runSpacing: 4, children: [ if (item.categoryName != null && item.categoryName!.isNotEmpty) _buildSmallBadge(item.categoryName!, const Color(0xFFD4AF37)), if (item.sku != null && item.sku!.isNotEmpty) _buildSmallBadge('SKU: ${item.sku}', Colors.blue), if (isItemCommodity && item.huid != null && item.huid!.isNotEmpty) _buildSmallBadge('HUID: ${item.huid}', Colors.purple), if (item.hsnCode != null && item.hsnCode!.isNotEmpty) _buildSmallBadge('HSN: ${item.hsnCode}', Colors.grey), if (!isItemCommodity) _buildSmallBadge('Unit: $unit', Colors.teal), ], ), ], ), ), IconButton( icon: const Icon(LucideIcons.edit2, size: 18, color: Colors.blue), tooltip: 'Edit Item', onPressed: () => _openAddItemSheet(editIndex: index), ), IconButton( icon: const Icon(LucideIcons.trash2, size: 18, color: Colors.red), tooltip: 'Remove Item', onPressed: () { setState(() => _items.removeAt(index)); }, ), ], ), const Divider(height: 20), if (isItemCommodity) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Metal: ${qtyOrWeight.toStringAsFixed(3)}$unit × ₹${item.unitPrice.toStringAsFixed(2)}', style: TextStyle(fontSize: 13, color: Colors.grey.shade600), ), Text( '₹${baseAmount.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13), ), ], ), if (makingChargeAmt > 0) ...[ 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: 13, color: Colors.grey.shade600), ), Text( '₹${makingChargeAmt.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13, color: Colors.indigo), ), ], ), ], ] else ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Rate: ₹${item.unitPrice.toStringAsFixed(2)} / $unit • Qty: ${qtyOrWeight.toStringAsFixed(qtyOrWeight.truncateToDouble() == qtyOrWeight ? 0 : 2)} $unit', style: TextStyle(fontSize: 13, color: Colors.grey.shade600, fontWeight: FontWeight.w500), ), Text( '₹${baseAmount.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13), ), ], ), ], if (item.discount > 0) ...[ const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Discount:', style: TextStyle(fontSize: 12, color: Colors.red)), Text('- ₹${item.discount.toStringAsFixed(2)}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.red)), ], ), ], if (item.taxRate > 0) ...[ const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'GST (${item.taxRate.toStringAsFixed(1)}%):', style: TextStyle(fontSize: 12, color: Colors.grey.shade500), ), Text( '+ ₹${taxAmount.toStringAsFixed(2)}', style: TextStyle(fontSize: 12, color: Colors.grey.shade600), ), ], ), ], const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Item Total:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)), Text( '₹${calculatedTotal.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.green), ), ], ), ], ), ); } Widget _buildSmallBadge(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(6), 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 _buildTotalsSummaryCard(bool isDark) { final businessProfile = ref.watch(businessProfileProvider).value; final businessStateId = businessProfile?.stateId; final products = ref.watch(productsProvider).value ?? []; final categories = ref.watch(productCategoriesProvider).value ?? []; final customers = ref.watch(customersProvider).value ?? []; final customer = _selectedCustomerId == null ? null : customers.where((c) => c.id == _selectedCustomerId).firstOrNull; final discountAmount = double.tryParse(_discountCtrl.text) ?? 0.0; final effectiveSupplyStateId = _selectedPlaceOfSupplyStateId ?? customer?.stateId ?? businessStateId; final isSameState = businessStateId != null && effectiveSupplyStateId != null && businessStateId == effectiveSupplyStateId; final hasCommodityItems = _items.any((item) { final product = products.where((p) => p.id == item.productId).firstOrNull; final category = categories.where((c) => c.name == item.categoryName || c.id == product?.categoryId).firstOrNull; return _isCommodityProduct(category: category, product: product, invoiceItem: item); }); double itemsSubtotal = 0; double makingChargesTotal = 0; double rawTaxableTotal = 0; double taxTotal = 0; for (var item in _items) { final product = products.where((p) => p.id == item.productId).firstOrNull; final category = categories.where((c) => c.name == item.categoryName || c.id == product?.categoryId).firstOrNull; final qtyOrWeight = item.weight != null && item.weight! > 0 ? item.weight! : (item.quantity > 0 ? item.quantity : 1.0); final baseAmt = qtyOrWeight * item.unitPrice; final isItemCommodity = _isCommodityProduct(category: category, product: product, invoiceItem: item); double makingAmt = 0.0; if (isItemCommodity) { if (item.makingChargesType == 'PERCENTAGE') { makingAmt = baseAmt * (item.makingCharge / 100.0); } else if (item.makingChargesType == 'PER_PIECE') { makingAmt = item.makingCharge; } else { makingAmt = qtyOrWeight * item.makingCharge; } } final itemTaxable = baseAmt + makingAmt + item.otherCharges - item.discount; final itemGstRate = item.taxRate > 0 ? item.taxRate : (product?.gstRate ?? (category?.defaultGst ?? (isItemCommodity ? 3.0 : 18.0))); final itemTax = (itemTaxable.clamp(0.0, double.infinity) * itemGstRate) / 100.0; itemsSubtotal += baseAmt; makingChargesTotal += makingAmt; rawTaxableTotal += (baseAmt + makingAmt + item.otherCharges); taxTotal += itemTax; } final effectiveTaxable = (rawTaxableTotal - discountAmount).clamp(0.0, double.infinity); if (rawTaxableTotal > 0 && discountAmount > 0) { taxTotal = taxTotal * (effectiveTaxable / rawTaxableTotal); } final grandTotal = effectiveTaxable + taxTotal; final effectiveGstRate = (effectiveTaxable > 0 && taxTotal > 0) ? (taxTotal / effectiveTaxable) * 100.0 : (_items.isNotEmpty ? _items.first.taxRate : (hasCommodityItems ? 3.0 : 18.0)); final cgstRate = effectiveGstRate / 2.0; final sgstRate = effectiveGstRate / 2.0; final igstRate = effectiveGstRate; String formatRate(double rate) { if (rate <= 0) return '0%'; final rounded = (rate * 100).round() / 100; if (rounded.truncateToDouble() == rounded) { return '${rounded.toInt()}%'; } return '${rounded.toStringAsFixed(1)}%'; } return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: isDark ? const Color(0xFF1E293B) : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: isDark ? Colors.white12 : Colors.grey.shade200), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.05), blurRadius: 15, offset: const Offset(0, 5), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('INVOICE SUMMARY', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, letterSpacing: 0.8, color: Colors.grey)), const SizedBox(height: 14), if (hasCommodityItems) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Metal Subtotal:'), Text('₹${itemsSubtotal.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600)), ], ), if (makingChargesTotal > 0) ...[ const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Making Charges:'), Text('₹${makingChargesTotal.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600)), ], ), ], ] else ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Items Subtotal (Taxable):'), Text('₹${itemsSubtotal.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600)), ], ), ], if (discountAmount > 0) ...[ const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Discount:', style: TextStyle(color: Colors.red, fontWeight: FontWeight.w600)), Text('- ₹${discountAmount.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.red)), ], ), ], const SizedBox(height: 8), if (isSameState) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('CGST (${formatRate(cgstRate)}):', style: TextStyle(color: Colors.grey.shade600)), Text('₹${(taxTotal / 2.0).toStringAsFixed(2)}'), ], ), const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('SGST (${formatRate(sgstRate)}):', style: TextStyle(color: Colors.grey.shade600)), Text('₹${(taxTotal / 2.0).toStringAsFixed(2)}'), ], ), ] else ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('IGST (${formatRate(igstRate)}):', style: TextStyle(color: Colors.grey.shade600)), Text('₹${taxTotal.toStringAsFixed(2)}'), ], ), ], const Divider(height: 24), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Grand Total:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), Text( '₹${grandTotal.toStringAsFixed(2)}', style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.green), ), ], ), ], ), ); } Widget _buildPaymentSection(bool isDark) { final wallets = ref.watch(walletProvider).value ?? []; final activeWallets = wallets.where((w) => w.nature == 'CASH' || w.nature == 'SAVINGS').toList(); // Compute grand total for live balance due final discountAmount = double.tryParse(_discountCtrl.text) ?? 0.0; final products = ref.watch(productsProvider).value ?? []; final categories = ref.watch(productCategoriesProvider).value ?? []; double rawTaxable = 0; double taxTotal = 0; for (var item in _items) { final product = products.where((p) => p.id == item.productId).firstOrNull; final category = categories.where((c) => c.name == item.categoryName || c.id == product?.categoryId).firstOrNull; final qtyOrWeight = item.weight != null && item.weight! > 0 ? item.weight! : (item.quantity > 0 ? item.quantity : 1.0); final baseAmt = qtyOrWeight * item.unitPrice; final isItemCommodity = _isCommodityProduct(category: category, product: product, invoiceItem: item); double makingAmt = 0.0; if (isItemCommodity) { if (item.makingChargesType == 'PERCENTAGE') { makingAmt = baseAmt * (item.makingCharge / 100.0); } else if (item.makingChargesType == 'PER_PIECE') { makingAmt = item.makingCharge; } else { makingAmt = qtyOrWeight * item.makingCharge; } } final itemTaxable = baseAmt + makingAmt + item.otherCharges - item.discount; final itemGstRate = item.taxRate > 0 ? item.taxRate : (product?.gstRate ?? (category?.defaultGst ?? (isItemCommodity ? 3.0 : 18.0))); final itemTax = (itemTaxable.clamp(0.0, double.infinity) * itemGstRate) / 100.0; rawTaxable += (baseAmt + makingAmt + item.otherCharges); taxTotal += itemTax; } final effectiveTaxable = (rawTaxable - discountAmount).clamp(0.0, double.infinity); if (rawTaxable > 0 && discountAmount > 0) { taxTotal = taxTotal * (effectiveTaxable / rawTaxable); } final grandTotal = effectiveTaxable + taxTotal; // If user has not manually changed amount paid and grandTotal > 0, auto-track grand total if (!_isAmountPaidEdited && grandTotal > 0 && _amountPaidCtrl.text != grandTotal.toStringAsFixed(2)) { _amountPaidCtrl.text = grandTotal.toStringAsFixed(2); } final enteredAmount = double.tryParse(_amountPaidCtrl.text) ?? (_isAmountPaidEdited ? 0.0 : grandTotal); final amountPaid = enteredAmount > grandTotal ? grandTotal : enteredAmount; final balanceDue = grandTotal - amountPaid; Widget? statusBadge; if (grandTotal > 0 && (_isAmountPaidEdited || _amountPaidCtrl.text.isNotEmpty)) { if (amountPaid >= grandTotal) { statusBadge = Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: Colors.green.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), ), child: const Text( 'Fully Paid', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.green), ), ); } else if (amountPaid > 0) { statusBadge = Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: Colors.orange.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), ), child: const Text( 'Partial Payment', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.orange), ), ); } else { statusBadge = Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: Colors.red.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), ), child: const Text( 'Unpaid', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.red), ), ); } } return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: isDark ? const Color(0xFF1E293B) : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: isDark ? Colors.white12 : Colors.grey.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('PAYMENT & SETTLEMENT', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, letterSpacing: 0.8, color: Colors.grey)), if (statusBadge != null) Flexible(child: statusBadge), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: PremiumTextField( controller: _amountPaidCtrl, labelText: 'Amount Received (₹)', keyboardType: const TextInputType.numberWithOptions(decimal: true), prefixIcon: const Icon(LucideIcons.indianRupee), onChanged: (val) { setState(() => _isAmountPaidEdited = true); }, ), ), const SizedBox(width: 12), Expanded( child: DropdownButtonFormField( value: _paymentMethod, decoration: InputDecoration( labelText: 'Method', border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), items: const [ DropdownMenuItem(value: 'Cash', child: Text('Cash')), DropdownMenuItem(value: 'UPI', child: Text('UPI / QR')), DropdownMenuItem(value: 'Bank Transfer', child: Text('Bank Transfer')), DropdownMenuItem(value: 'Card', child: Text('Card')), ], onChanged: (val) => setState(() => _paymentMethod = val!), ), ), ], ), const SizedBox(height: 14), if (activeWallets.isNotEmpty) DropdownButtonFormField( value: _selectedWalletId ?? activeWallets.first.id, decoration: InputDecoration( labelText: 'Deposit To Account', prefixIcon: const Icon(LucideIcons.wallet), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), items: activeWallets.map((w) => DropdownMenuItem( value: w.id, child: Text('${w.name} (₹${w.balance.toStringAsFixed(0)})'), )).toList(), onChanged: (val) => setState(() => _selectedWalletId = val), ), if (balanceDue > 0) ...[ const SizedBox(height: 14), Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: (amountPaid > 0 ? Colors.orange : Colors.red).withValues(alpha: 0.08), borderRadius: BorderRadius.circular(12), border: Border.all(color: (amountPaid > 0 ? Colors.orange : Colors.red).withValues(alpha: 0.25)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon( amountPaid > 0 ? LucideIcons.circleDot : LucideIcons.alertCircle, size: 16, color: amountPaid > 0 ? Colors.orange : Colors.red, ), const SizedBox(width: 8), Text( amountPaid > 0 ? 'Remaining Balance Due:' : 'Total Amount Due:', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: isDark ? Colors.white70 : Colors.black87, ), ), ], ), Text( '₹${balanceDue.toStringAsFixed(2)}', style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: amountPaid > 0 ? Colors.orange.shade700 : Colors.red.shade700, ), ), ], ), ), const SizedBox(height: 14), InkWell( onTap: () async { final picked = await showDatePicker( context: context, initialDate: _nextPaymentDate ?? DateTime.now().add(const Duration(days: 30)), firstDate: DateTime.now(), lastDate: DateTime(2100), ); if (picked != null) { setState(() => _nextPaymentDate = picked); } }, child: InputDecorator( decoration: InputDecoration( labelText: 'Next Due / Balance Settlement Date', prefixIcon: const Icon(LucideIcons.calendarClock), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), child: Text( _nextPaymentDate != null ? DateFormat('dd MMM yyyy').format(_nextPaymentDate!) : 'Select Settlement Date (Default: 30 Days)', style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13), ), ), ), ], const Divider(height: 28), // EMI Option Toggle Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Row( children: [ Icon(LucideIcons.creditCard, size: 18, color: Colors.purple), SizedBox(width: 8), Text('Enable EMI Installments', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)), ], ), Switch( value: _isEmi, activeColor: Colors.purple, onChanged: (val) => setState(() => _isEmi = val), ), ], ), if (_isEmi) ...[ const SizedBox(height: 12), Row( children: [ Expanded( child: PremiumTextField( controller: _emiAmountCtrl, labelText: 'EMI Amount (₹)', keyboardType: const TextInputType.numberWithOptions(decimal: true), prefixIcon: const Icon(LucideIcons.indianRupee), ), ), const SizedBox(width: 12), Expanded( child: DropdownButtonFormField( value: _emiCycle, decoration: InputDecoration( labelText: 'Cycle', border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), items: const [ DropdownMenuItem(value: 'MONTHLY', child: Text('Monthly')), DropdownMenuItem(value: 'WEEKLY', child: Text('Weekly')), ], onChanged: (val) => setState(() => _emiCycle = val!), ), ), ], ), ], ], ), ); } Future _scanTrackingBarcode() async { final code = await Navigator.push( context, MaterialPageRoute(builder: (_) => const BarcodeScannerScreen()), ); if (code != null && code.isNotEmpty) { setState(() { _trackingNumberCtrl.text = code.trim(); if (_dispatchStatus == 'PENDING') { _dispatchStatus = 'SHIPPED'; _shippedAt = DateTime.now(); } }); } } Widget _buildShippingLogisticsSection(bool isDark) { final customers = ref.watch(customersProvider).value ?? []; final customer = _selectedCustomerId == null ? null : customers.where((c) => c.id == _selectedCustomerId).firstOrNull; final courierSuggestions = [ 'Delhivery', 'BlueDart', 'Shiprocket', 'DTDC', 'India Post', 'Amazon Shipping', 'Ekart', 'Shadowfax', 'Direct / Hand Delivery', ]; final dispatchStatuses = [ {'status': 'PENDING', 'label': 'Pending', 'color': Colors.grey}, {'status': 'PACKED', 'label': 'Packed', 'color': Colors.blue}, {'status': 'SHIPPED', 'label': 'Shipped', 'color': Colors.indigo}, {'status': 'IN_TRANSIT', 'label': 'In Transit', 'color': Colors.amber.shade800}, {'status': 'DELIVERED', 'label': 'Delivered', 'color': Colors.green}, {'status': 'RTO', 'label': 'RTO / Return', 'color': Colors.red}, ]; return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: isDark ? const Color(0xFF1E293B) : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: isDark ? Colors.white12 : Colors.grey.shade200), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.05), blurRadius: 15, offset: const Offset(0, 5), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.indigo.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8), ), child: const Icon(LucideIcons.truck, size: 16, color: Colors.indigo), ), const SizedBox(width: 8), const Text( 'SHIPPING & COURIER LOGISTICS', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, letterSpacing: 0.8, color: Colors.grey), ), ], ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: Colors.indigo.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.indigo.withValues(alpha: 0.3)), ), child: Text( _dispatchStatus, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.indigo), ), ), ], ), const SizedBox(height: 16), // Dispatch Status Selector const Text('Dispatch Status:', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.grey)), const SizedBox(height: 6), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: dispatchStatuses.map((st) { final statusKey = st['status'] as String; final statusLabel = st['label'] as String; final statusColor = st['color'] as Color; final isSelected = _dispatchStatus == statusKey; return Padding( padding: const EdgeInsets.only(right: 8.0), child: ChoiceChip( label: Text( statusLabel, style: TextStyle( fontSize: 12, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected ? Colors.white : (isDark ? Colors.white70 : Colors.black87), ), ), selected: isSelected, selectedColor: statusColor, backgroundColor: isDark ? const Color(0xFF0F172A) : Colors.grey.shade100, onSelected: (selected) { if (selected) { setState(() { _dispatchStatus = statusKey; if (statusKey == 'SHIPPED' && _shippedAt == null) { _shippedAt = DateTime.now(); } }); } }, ), ); }).toList(), ), ), const SizedBox(height: 14), // Courier Partner & Quick Selection Chips Row( children: [ Expanded( child: PremiumTextField( controller: _courierPartnerCtrl, labelText: 'Courier Partner (e.g. Delhivery, BlueDart)', prefixIcon: const Icon(LucideIcons.packageCheck), ), ), ], ), const SizedBox(height: 6), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: courierSuggestions.map((name) { final isCurrent = _courierPartnerCtrl.text.trim().toLowerCase() == name.toLowerCase(); return Padding( padding: const EdgeInsets.only(right: 6.0), child: ActionChip( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0), label: Text(name, style: TextStyle(fontSize: 11, color: isCurrent ? Colors.blue : null)), backgroundColor: isCurrent ? Colors.blue.withValues(alpha: 0.15) : null, onPressed: () { setState(() { _courierPartnerCtrl.text = name; }); }, ), ); }).toList(), ), ), const SizedBox(height: 14), // AWB / Tracking Number with Scanner Button Row( children: [ Expanded( child: PremiumTextField( controller: _trackingNumberCtrl, labelText: 'AWB / Tracking Number', prefixIcon: const Icon(LucideIcons.barcode), ), ), const SizedBox(width: 8), IconButton.filledTonal( icon: const Icon(LucideIcons.scanLine), tooltip: 'Scan AWB Barcode', onPressed: _scanTrackingBarcode, ), ], ), const SizedBox(height: 14), // Delivery Address Toggle & Form CheckboxListTile( contentPadding: EdgeInsets.zero, dense: true, title: const Text('Deliver to customer billing address', style: TextStyle(fontWeight: FontWeight.w600)), subtitle: Text( customer?.address != null && customer!.address!.isNotEmpty ? customer.address! : 'Customer address on file', style: TextStyle(fontSize: 11, color: Colors.grey.shade600), maxLines: 1, overflow: TextOverflow.ellipsis, ), value: _sameAsCustomerAddress, onChanged: (val) { setState(() => _sameAsCustomerAddress = val ?? true); }, ), if (!_sameAsCustomerAddress) ...[ const SizedBox(height: 8), PremiumTextField( controller: _shippingAddressCtrl, labelText: 'Shipping / Delivery Address', maxLines: 2, prefixIcon: const Icon(LucideIcons.mapPin), ), const SizedBox(height: 10), PremiumTextField( controller: _shippingPincodeCtrl, labelText: 'Shipping Pincode', keyboardType: TextInputType.number, prefixIcon: const Icon(LucideIcons.hash), ), ], ], ), ); } } class _AddSalesItemSheet extends ConsumerStatefulWidget { final ValueChanged onItemAdded; final InvoiceItem? initialItem; const _AddSalesItemSheet({ required this.onItemAdded, this.initialItem, }); @override ConsumerState<_AddSalesItemSheet> createState() => _AddSalesItemSheetState(); } class _AddSalesItemSheetState extends ConsumerState<_AddSalesItemSheet> { final _searchCtrl = TextEditingController(); Product? _selectedProduct; InventoryItem? _selectedInventoryItem; double _selectedProductAvailableStock = 0.0; final _unitPriceCtrl = TextEditingController(); final _quantityCtrl = TextEditingController(text: '1'); final _weightCtrl = TextEditingController(text: '1.000'); final _makingChargeCtrl = TextEditingController(text: '0.0'); String _makingChargeType = 'PER_GRAM'; final _discountCtrl = TextEditingController(text: '0.0'); @override void initState() { super.initState(); Future.microtask(() => ref.invalidate(inventoryItemsProvider)); if (widget.initialItem != null) { final it = widget.initialItem!; _makingChargeCtrl.text = it.makingCharge.toStringAsFixed(2); _makingChargeType = it.makingChargesType ?? 'PER_GRAM'; _discountCtrl.text = it.discount.toStringAsFixed(2); _unitPriceCtrl.text = it.unitPrice.toStringAsFixed(2); _quantityCtrl.text = (it.quantity > 0 ? it.quantity : 1.0).toStringAsFixed(it.quantity.truncateToDouble() == it.quantity ? 0 : 2); _weightCtrl.text = (it.weight ?? 1.0).toStringAsFixed(3); WidgetsBinding.instance.addPostFrameCallback((_) { final products = ref.read(productsProvider).value ?? []; final invItems = ref.read(inventoryItemsProvider).value ?? []; setState(() { _selectedProduct = products.where((p) => p.id == it.productId).firstOrNull ?? Product( id: it.productId, name: it.productName ?? 'Item', sku: it.sku, hsnCode: it.hsnCode, gstRate: it.taxRate, sellingPrice: it.unitPrice, makingCharges: it.makingCharge, makingChargesType: it.makingChargesType, ); if (it.inventoryItemId != null) { _selectedInventoryItem = invItems.where((i) => i.id == it.inventoryItemId).firstOrNull; } }); }); } } @override void dispose() { _searchCtrl.dispose(); _unitPriceCtrl.dispose(); _quantityCtrl.dispose(); _weightCtrl.dispose(); _makingChargeCtrl.dispose(); _discountCtrl.dispose(); super.dispose(); } void _onProductSelected( Product product, ProductCategory? category, double liveCommodityRate, bool isCommodity, { double? avgPrice, double? availableStock, }) { setState(() { _selectedProduct = product; _selectedInventoryItem = null; _selectedProductAvailableStock = availableStock ?? product.currentStock ?? 0.0; final resolvedPrice = (avgPrice != null && avgPrice > 0) ? avgPrice : (product.sellingPrice ?? 0.0); _unitPriceCtrl.text = resolvedPrice > 0 ? resolvedPrice.toStringAsFixed(2) : ''; _quantityCtrl.text = '1'; _weightCtrl.text = '1.000'; if (product.makingCharges != null && product.makingCharges! > 0) { _makingChargeCtrl.text = product.makingCharges!.toStringAsFixed(2); _makingChargeType = product.makingChargesType ?? (category?.makingChargeType ?? 'PER_GRAM'); } else if (category?.defaultMakingCharge != null && category!.defaultMakingCharge! > 0) { _makingChargeCtrl.text = category.defaultMakingCharge!.toStringAsFixed(2); _makingChargeType = category.makingChargeType ?? 'PER_GRAM'; } else { _makingChargeCtrl.text = '0.0'; _makingChargeType = category?.makingChargeType ?? 'PER_GRAM'; } }); } void _onInventoryItemSelected(InventoryItem item, Product product, ProductCategory? category, bool isCommodity) { setState(() { _selectedProduct = product; _selectedInventoryItem = item; _selectedProductAvailableStock = item.grossWeight ?? item.netWeight ?? 1.0; final resolvedPrice = item.saleRate ?? product.sellingPrice ?? 0.0; _unitPriceCtrl.text = resolvedPrice > 0 ? resolvedPrice.toStringAsFixed(2) : ''; _quantityCtrl.text = '1'; _weightCtrl.text = (item.grossWeight ?? 1.0).toStringAsFixed(3); if (item.makingCharges != null && item.makingCharges! > 0) { _makingChargeCtrl.text = item.makingCharges!.toStringAsFixed(2); _makingChargeType = item.makingChargeType ?? 'PER_GRAM'; } else if (product.makingCharges != null && product.makingCharges! > 0) { _makingChargeCtrl.text = product.makingCharges!.toStringAsFixed(2); _makingChargeType = product.makingChargesType ?? (category?.makingChargeType ?? 'PER_GRAM'); } else if (category?.defaultMakingCharge != null && category!.defaultMakingCharge! > 0) { _makingChargeCtrl.text = category.defaultMakingCharge!.toStringAsFixed(2); _makingChargeType = category.makingChargeType ?? 'PER_GRAM'; } else { _makingChargeCtrl.text = '0.0'; _makingChargeType = category?.makingChargeType ?? 'PER_GRAM'; } }); } Future _scanBarcodeInSheet() async { final scanned = await Navigator.push( context, MaterialPageRoute(builder: (_) => const BarcodeScannerScreen()), ); if (scanned != null && scanned.isNotEmpty) { _searchCtrl.text = scanned; setState(() {}); } } void _submitItem(ProductCategory? category, double unitRate, bool isCommodity, String uomUnit) { if (_selectedProduct == null) return; final discount = double.tryParse(_discountCtrl.text) ?? 0.0; final gstRate = _selectedProduct?.gstRate ?? (category?.defaultGst ?? (isCommodity ? 3.0 : 18.0)); double effectivePrice = unitRate; double qty = 1.0; double? weight; double makingCharge = 0.0; String makingType = 'PER_PIECE'; double lineTaxable = 0.0; double taxAmount = 0.0; double total = 0.0; if (isCommodity) { weight = double.tryParse(_weightCtrl.text) ?? (_selectedInventoryItem?.grossWeight ?? 1.0); qty = 1.0; effectivePrice = unitRate; makingCharge = double.tryParse(_makingChargeCtrl.text) ?? 0.0; makingType = _makingChargeType; final metalAmount = weight * effectivePrice; double makingAmt = 0.0; if (makingType == 'PERCENTAGE') { makingAmt = metalAmount * (makingCharge / 100.0); } else if (makingType == 'PER_PIECE') { makingAmt = makingCharge; } else { makingAmt = weight * makingCharge; } lineTaxable = metalAmount + makingAmt - discount; taxAmount = (lineTaxable.clamp(0.0, double.infinity) * gstRate) / 100.0; total = lineTaxable.clamp(0.0, double.infinity) + taxAmount; } else { effectivePrice = double.tryParse(_unitPriceCtrl.text) ?? (_selectedProduct?.sellingPrice ?? unitRate); qty = double.tryParse(_quantityCtrl.text) ?? 1.0; weight = null; makingCharge = 0.0; makingType = 'PER_PIECE'; lineTaxable = (qty * effectivePrice) - discount; taxAmount = (lineTaxable.clamp(0.0, double.infinity) * gstRate) / 100.0; total = lineTaxable.clamp(0.0, double.infinity) + taxAmount; } final item = InvoiceItem( productId: _selectedProduct!.id, inventoryItemId: _selectedInventoryItem?.id, productName: _selectedProduct!.name, categoryName: category?.name, commodityCode: isCommodity ? category?.commodityCode : null, hsnCode: category?.defaultHsn ?? _selectedProduct?.hsnCode, sku: _selectedInventoryItem?.sku ?? _selectedProduct?.sku, huid: isCommodity ? _selectedInventoryItem?.huid : null, description: _selectedProduct!.name, quantity: qty, weight: weight, unitPrice: effectivePrice, makingCharge: makingCharge, makingChargesType: makingType, taxRate: gstRate, discount: discount, photoUrl: _selectedInventoryItem?.tagNumber, total: total, ); widget.onItemAdded(item); Navigator.pop(context); } @override Widget build(BuildContext context) { final products = ref.watch(productsProvider).value ?? []; final categories = ref.watch(productCategoriesProvider).value ?? []; final commodityRates = ref.watch(commodityRatesProvider).value ?? []; final inventoryItems = ref.watch(inventoryItemsProvider).value ?? []; final uoms = ref.watch(uomsProvider).value ?? []; final businessProfile = ref.watch(businessProfileProvider).value; final nature = (businessProfile?.natureOfBusiness ?? businessProfile?.industry ?? 'JEWELLERY').toUpperCase(); final isJewellery = nature == 'JEWELLERY'; final isDark = Theme.of(context).brightness == Brightness.dark; ProductCategory? category; if (_selectedProduct?.categoryId != null) { category = categories.where((c) => c.id == _selectedProduct!.categoryId).firstOrNull; } final isCommodity = _isCommodityProduct( category: category, product: _selectedProduct, inventoryItem: _selectedInventoryItem, ); final uomUnit = _resolveProductUnit(product: _selectedProduct, category: category, uoms: uoms, isCommodity: isCommodity); double commodityRate = 0.0; if (category?.commodityCode != null) { final match = commodityRates.where((r) => r.commodityCode.toUpperCase() == category!.commodityCode!.toUpperCase()).firstOrNull; if (match != null) { commodityRate = match.rate; } } final purityFactor = resolvePurity(categoryPurity: category?.purityFactor, productPurity: _selectedProduct?.purityFactor); final effectiveUnitRate = isCommodity ? (commodityRate > 0 ? (commodityRate * purityFactor) : (_selectedProduct?.sellingPrice ?? 0.0)) : (double.tryParse(_unitPriceCtrl.text) ?? (_selectedProduct?.sellingPrice ?? 0.0)); final weight = double.tryParse(_weightCtrl.text) ?? (_selectedInventoryItem?.grossWeight ?? 1.0); final quantity = double.tryParse(_quantityCtrl.text) ?? 1.0; final discount = double.tryParse(_discountCtrl.text) ?? 0.0; final gstRate = _selectedProduct?.gstRate ?? (category?.defaultGst ?? (isCommodity ? 3.0 : 18.0)); double taxable = 0.0; double makingAmt = 0.0; double metalAmount = 0.0; if (isCommodity) { metalAmount = weight * effectiveUnitRate; final makingCharge = double.tryParse(_makingChargeCtrl.text) ?? 0.0; if (_makingChargeType == 'PERCENTAGE') { makingAmt = metalAmount * (makingCharge / 100.0); } else if (_makingChargeType == 'PER_PIECE') { makingAmt = makingCharge; } else { makingAmt = weight * makingCharge; } taxable = (metalAmount + makingAmt - discount).clamp(0.0, double.infinity); } else { final subtotal = quantity * effectiveUnitRate; taxable = (subtotal - discount).clamp(0.0, double.infinity); } final taxAmount = (taxable * gstRate) / 100.0; final total = taxable + taxAmount; final query = _searchCtrl.text.trim().toLowerCase(); final availableItems = inventoryItems .where((item) => item.status == null || item.status == 'AVAILABLE') .toList(); availableItems.sort((a, b) { if (a.createdAt == null && b.createdAt == null) return 0; if (a.createdAt == null) return 1; if (b.createdAt == null) return -1; return b.createdAt!.compareTo(a.createdAt!); }); final List displayInventoryItems; if (query.isEmpty) { displayInventoryItems = availableItems.take(20).toList(); } else { displayInventoryItems = availableItems.where((item) { final p = products.where((pr) => pr.id == item.productId).firstOrNull; final cat = p != null ? categories.where((c) => c.id == p.categoryId).firstOrNull : null; final matchesHuid = item.huid != null && item.huid!.toLowerCase().contains(query); final matchesSku = (item.sku != null && item.sku!.toLowerCase().contains(query)) || (p?.sku != null && p!.sku!.toLowerCase().contains(query)); final matchesName = p != null && p.name.toLowerCase().contains(query); final matchesBarcode = p != null && p.barcode != null && p.barcode!.toLowerCase().contains(query); final matchesCategory = cat != null && cat.name.toLowerCase().contains(query); final matchesTag = item.tagNumber != null && item.tagNumber!.toLowerCase().contains(query); return matchesHuid || matchesSku || matchesName || matchesBarcode || matchesCategory || matchesTag; }).take(20).toList(); } final matchingProductIdsInInv = displayInventoryItems.map((i) => i.productId).toSet(); final directProducts = products.where((p) { if (matchingProductIdsInInv.contains(p.id)) return false; if (query.isEmpty) return true; final q = query.toLowerCase(); final cat = categories.where((c) => c.id == p.categoryId).firstOrNull; return p.name.toLowerCase().contains(q) || (p.sku != null && p.sku!.toLowerCase().contains(q)) || (p.barcode != null && p.barcode!.toLowerCase().contains(q)) || (cat != null && cat.name.toLowerCase().contains(q)); }).take(20).toList(); final matchedProducts = products.where((p) { if (query.isEmpty) return true; final cat = categories.where((c) => c.id == p.categoryId).firstOrNull; return p.name.toLowerCase().contains(query) || (p.sku != null && p.sku!.toLowerCase().contains(query)) || (p.barcode != null && p.barcode!.toLowerCase().contains(query)) || (cat != null && cat.name.toLowerCase().contains(query)); }).toList(); final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; return AnimatedPadding( padding: EdgeInsets.only(bottom: keyboardHeight), duration: const Duration(milliseconds: 150), curve: Curves.easeOut, child: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), behavior: HitTestBehavior.translucent, child: Container( height: MediaQuery.of(context).size.height * 0.88, decoration: BoxDecoration( color: Theme.of(context).scaffoldBackgroundColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), ), child: Column( children: [ Center( child: Container( margin: const EdgeInsets.only(top: 12, bottom: 8), width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(2), ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Add Item to Invoice', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), Row( mainAxisSize: MainAxisSize.min, children: [ if (keyboardHeight > 0) TextButton.icon( style: TextButton.styleFrom( backgroundColor: Colors.blue.withValues(alpha: 0.12), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), icon: const Icon(LucideIcons.keyboard, size: 16, color: Colors.blue), label: const Text('Done', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue)), onPressed: () => FocusScope.of(context).unfocus(), ), IconButton( icon: const Icon(LucideIcons.x, size: 20), onPressed: () => Navigator.pop(context), ), ], ), ], ), ), const Divider(height: 1), // Keyboard Toolbar when active on iOS/Android if (keyboardHeight > 0) Container( color: isDark ? const Color(0xFF1E293B) : Colors.grey.shade100, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Editing ${_selectedProduct?.name ?? 'Item'}', style: TextStyle(fontSize: 12, color: Colors.grey.shade600, fontWeight: FontWeight.w500), ), TextButton.icon( style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), backgroundColor: Colors.blue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), icon: const Icon(LucideIcons.check, size: 15), label: const Text('Done / Dismiss', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), onPressed: () => FocusScope.of(context).unfocus(), ), ], ), ), Expanded( child: ListView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, padding: const EdgeInsets.fromLTRB(20, 16, 20, 40), children: [ // Search Input with Barcode Action & Rounded Borders TextField( controller: _searchCtrl, decoration: InputDecoration( labelText: isJewellery ? 'Search Product by HUID / Name / SKU / Barcode' : 'Search Product by Name / SKU / Barcode', prefixIcon: const Icon(LucideIcons.search), suffixIcon: Row( mainAxisSize: MainAxisSize.min, children: [ if (_searchCtrl.text.isNotEmpty) IconButton( icon: const Icon(LucideIcons.x), onPressed: () => setState(() => _searchCtrl.clear()), ), IconButton( icon: const Icon(LucideIcons.qrCode, color: Colors.blue), tooltip: 'Scan Barcode', onPressed: _scanBarcodeInSheet, ), ], ), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), onChanged: (val) => setState(() {}), ), const SizedBox(height: 12), if (_selectedProduct == null) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( !isJewellery ? (query.isEmpty ? 'Available Products in Catalog (${matchedProducts.length}):' : 'Matched Products (${matchedProducts.length}):') : (query.isEmpty ? 'Available Stock in Inventory (${displayInventoryItems.length + directProducts.length}):' : 'Matched Items (${displayInventoryItems.length + directProducts.length}):'), style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: isDark ? Colors.grey.shade400 : Colors.grey.shade700, ), ), ], ), const SizedBox(height: 10), if (!isJewellery) ...[ // NON-JEWELLERY MODULE: Show List of Unique Products with Weighted Avg Price & Aggregated Available Stock if (matchedProducts.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 40.0), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(LucideIcons.packageOpen, size: 40, color: Colors.grey.shade400), const SizedBox(height: 10), Text( query.isEmpty ? 'No products available in catalog' : 'No matching products found', style: TextStyle(color: Colors.grey.shade500, fontSize: 14), ), ], ), ), ) else ...[ ...matchedProducts.map((p) { final cat = categories.where((c) => c.id == p.categoryId).firstOrNull; final itemUom = _resolveProductUnit(product: p, category: cat, uoms: uoms, isCommodity: false); // Find all available batches for this product in inventory final pBatches = availableItems.where((i) => i.productId == p.id).toList(); double totalAvailableStock = 0.0; double totalCostOrPrice = 0.0; for (var batch in pBatches) { final qty = (batch.grossWeight != null && batch.grossWeight! > 0) ? batch.grossWeight! : ((batch.netWeight != null && batch.netWeight! > 0) ? batch.netWeight! : 1.0); final rate = (batch.saleRate != null && batch.saleRate! > 0) ? batch.saleRate! : (batch.purchaseCost != null && batch.purchaseCost! > 0 ? batch.purchaseCost! : (p.sellingPrice ?? 0.0)); totalAvailableStock += qty; totalCostOrPrice += (qty * rate); } if (pBatches.isEmpty && (p.currentStock != null && p.currentStock! > 0)) { totalAvailableStock = p.currentStock!; totalCostOrPrice = totalAvailableStock * (p.sellingPrice ?? 0.0); } final avgPrice = totalAvailableStock > 0 ? (totalCostOrPrice / totalAvailableStock) : (p.sellingPrice ?? 0.0); return _buildProductSearchCard( product: p, category: cat, availableStock: totalAvailableStock, avgPrice: avgPrice, uomUnit: itemUom, isDark: isDark, onTap: () => _onProductSelected( p, cat, 0.0, false, avgPrice: avgPrice, availableStock: totalAvailableStock, ), ); }), ], ] else ...[ // JEWELLERY MODULE: Show Serialized Inventory Items (HUID, Tag No, Purity, Gross Weight) if (displayInventoryItems.isEmpty && directProducts.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 40.0), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(LucideIcons.packageOpen, size: 40, color: Colors.grey.shade400), const SizedBox(height: 10), Text( query.isEmpty ? 'No items available in stock' : 'No matching items found', style: TextStyle(color: Colors.grey.shade500, fontSize: 14), ), ], ), ), ) else ...[ ...displayInventoryItems.map((item) { final p = products.where((pr) => pr.id == item.productId).firstOrNull ?? Product( id: item.productId, name: item.sku ?? 'Inventory Item', sku: item.sku, ); final cat = categories.where((c) => c.id == p.categoryId).firstOrNull; final itemIsCommodity = _isCommodityProduct(category: cat, product: p, inventoryItem: item); final itemUom = _resolveProductUnit(product: p, category: cat, uoms: uoms, isCommodity: itemIsCommodity); double commRate = 0.0; if (cat?.commodityCode != null) { final m = commodityRates.where((r) => r.commodityCode.toUpperCase() == cat!.commodityCode!.toUpperCase()).firstOrNull; if (m != null) commRate = m.rate; } return _buildSearchItemCard( product: p, category: cat, inventoryItem: item, commodityRate: commRate, isCommodity: itemIsCommodity, uomUnit: itemUom, isDark: isDark, onTap: () => _onInventoryItemSelected(item, p, cat, itemIsCommodity), ); }), ...directProducts.map((p) { final cat = categories.where((c) => c.id == p.categoryId).firstOrNull; final itemIsCommodity = _isCommodityProduct(category: cat, product: p); final itemUom = _resolveProductUnit(product: p, category: cat, uoms: uoms, isCommodity: itemIsCommodity); double commRate = 0.0; if (cat?.commodityCode != null) { final m = commodityRates.where((r) => r.commodityCode.toUpperCase() == cat!.commodityCode!.toUpperCase()).firstOrNull; if (m != null) commRate = m.rate; } return _buildSearchItemCard( product: p, category: cat, inventoryItem: null, commodityRate: commRate, isCommodity: itemIsCommodity, uomUnit: itemUom, isDark: isDark, onTap: () => _onProductSelected(p, cat, commRate, itemIsCommodity), ); }), ], ], ] else ...[ // SELECTED PRODUCT DETAILS CARD Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? const Color(0xFF1E293B) : Colors.blue.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.blue.withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( _selectedProduct!.name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ), TextButton( onPressed: () => setState(() { _selectedProduct = null; _selectedInventoryItem = null; }), child: const Text('Change'), ), ], ), const SizedBox(height: 6), Wrap( spacing: 8, runSpacing: 4, children: [ if (category?.name != null) _buildSheetBadge(category!.name, const Color(0xFFD4AF37)), if (_selectedProduct!.sku != null) _buildSheetBadge('SKU: ${_selectedProduct!.sku}', Colors.blue), if (isCommodity && _selectedInventoryItem?.huid != null) _buildSheetBadge('HUID: ${_selectedInventoryItem!.huid}', Colors.purple), if (isCommodity && category?.commodityCode != null) _buildSheetBadge('${category!.commodityCode} (${formatPurity(purityFactor)})', Colors.teal), if (isCommodity) _buildSheetBadge('Purity: ${formatPurity(purityFactor)}', Colors.amber.shade800), if (!isCommodity) ...[ _buildSheetBadge('Unit: $uomUnit', Colors.teal), if (_selectedProductAvailableStock > 0) _buildSheetBadge('Available Stock: ${_selectedProductAvailableStock.toStringAsFixed(_selectedProductAvailableStock.truncateToDouble() == _selectedProductAvailableStock ? 0 : 2)} $uomUnit', Colors.green.shade800), ], ], ), ], ), ), const SizedBox(height: 16), if (isCommodity) ...[ // COMMODITY / JEWELLERY FORM Row( children: [ Expanded( child: TextFormField( initialValue: category?.defaultHsn ?? _selectedProduct?.hsnCode ?? '7113', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), enabled: false, decoration: InputDecoration( labelText: 'HSN Code', prefixIcon: const Icon(LucideIcons.hash, size: 18), contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), ), const SizedBox(width: 12), Expanded( child: PremiumTextField( controller: _weightCtrl, labelText: 'Weight ($uomUnit)', keyboardType: const TextInputType.numberWithOptions(decimal: true), prefixIcon: const Icon(LucideIcons.scale, size: 18), onChanged: (_) => setState(() {}), ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: TextFormField( key: ValueKey(effectiveUnitRate), initialValue: '₹${effectiveUnitRate.toStringAsFixed(2)}/$uomUnit', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), enabled: false, decoration: InputDecoration( labelText: 'Live Rate', prefixIcon: const Icon(LucideIcons.trendingUp, size: 18), contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), ), const SizedBox(width: 12), Expanded( child: TextFormField( key: ValueKey(metalAmount), initialValue: '₹${metalAmount.toStringAsFixed(2)}', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), enabled: false, decoration: InputDecoration( labelText: 'Metal Amount', prefixIcon: const Icon(LucideIcons.coins, size: 18), contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), ), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: PremiumTextField( controller: _makingChargeCtrl, labelText: 'Making Charges', keyboardType: const TextInputType.numberWithOptions(decimal: true), prefixIcon: const Icon(LucideIcons.hammer), onChanged: (_) => setState(() {}), ), ), const SizedBox(width: 12), Expanded( child: DropdownButtonFormField( value: _makingChargeType, decoration: InputDecoration( labelText: 'Type', border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), items: const [ DropdownMenuItem(value: 'PER_GRAM', child: Text('Per Gram')), DropdownMenuItem(value: 'PER_PIECE', child: Text('Per Piece')), DropdownMenuItem(value: 'PERCENTAGE', child: Text('Percentage %')), ], onChanged: (val) => setState(() => _makingChargeType = val!), ), ), ], ), if (makingAmt > 0) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.indigo.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.indigo.withValues(alpha: 0.2)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Calculated Making Charge:', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500), ), Text( '₹${makingAmt.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.indigo), ), ], ), ), ], const SizedBox(height: 12), PremiumTextField( controller: _discountCtrl, labelText: 'Item Discount (₹)', keyboardType: const TextInputType.numberWithOptions(decimal: true), prefixIcon: const Icon(LucideIcons.tag), onChanged: (_) => setState(() {}), ), ] else ...[ // E-COMMERCE / GENERAL / NON-COMMODITY FORM Row( children: [ Expanded( child: PremiumTextField( controller: _unitPriceCtrl, labelText: 'Unit Price (₹ / $uomUnit)', keyboardType: const TextInputType.numberWithOptions(decimal: true), prefixIcon: const Icon(LucideIcons.indianRupee), onChanged: (_) => setState(() {}), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ PremiumTextField( controller: _quantityCtrl, labelText: 'Quantity ($uomUnit)', keyboardType: const TextInputType.numberWithOptions(decimal: true), prefixIcon: const Icon(LucideIcons.package), onChanged: (_) => setState(() {}), ), if (_selectedProductAvailableStock > 0 && (double.tryParse(_quantityCtrl.text) ?? 0.0) > _selectedProductAvailableStock) ...[ const SizedBox(height: 4), Text( 'Exceeds available stock (${_selectedProductAvailableStock.toStringAsFixed(_selectedProductAvailableStock.truncateToDouble() == _selectedProductAvailableStock ? 0 : 2)} $uomUnit)', style: const TextStyle(fontSize: 11, color: Colors.orange, fontWeight: FontWeight.w600), ), ], ], ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: PremiumTextField( controller: _discountCtrl, labelText: 'Item Discount (₹)', keyboardType: const TextInputType.numberWithOptions(decimal: true), prefixIcon: const Icon(LucideIcons.tag), onChanged: (_) => setState(() {}), ), ), const SizedBox(width: 12), Expanded( child: TextFormField( initialValue: category?.defaultHsn ?? _selectedProduct?.hsnCode ?? '-', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), enabled: false, decoration: InputDecoration( labelText: 'HSN Code', prefixIcon: const Icon(LucideIcons.hash, size: 18), contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), ), ], ), ], const SizedBox(height: 16), // Calculated Subtotal & GST Display Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? Colors.black26 : Colors.grey.shade100, borderRadius: BorderRadius.circular(14), ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Taxable Subtotal:'), Text('₹${taxable.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold)), ], ), const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('GST (${gstRate.toStringAsFixed(1)}%):'), Text('+ ₹${taxAmount.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.indigo)), ], ), const Divider(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Item Total:', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), Text('₹${total.toStringAsFixed(2)}', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.green)), ], ), ], ), ), const SizedBox(height: 24), ElevatedButton( onPressed: () => _submitItem(category, effectiveUnitRate, isCommodity, uomUnit), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), backgroundColor: Colors.blue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), child: const Text('Add to Invoice', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), ), ], ], ), ), ], ), ), ), ); } Widget _buildSheetBadge(String text, Color color) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(6), border: Border.all(color: color.withValues(alpha: 0.4)), ), child: Text( text, style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: color), ), ); } Widget _buildProductSearchCard({ required Product product, ProductCategory? category, required double availableStock, required double avgPrice, required String uomUnit, required bool isDark, required VoidCallback onTap, }) { final gstRate = product.gstRate ?? (category?.defaultGst ?? 18.0); final unitWithGst = avgPrice > 0 ? avgPrice * (1 + gstRate / 100.0) : 0.0; final totalStockValue = availableStock * avgPrice; final sku = product.sku; return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: isDark ? const Color(0xFF1E293B) : Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: isDark ? Colors.white12 : Colors.grey.shade200), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.04), blurRadius: 8, offset: const Offset(0, 3), ), ], ), child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(16), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(10), ), child: const Icon( LucideIcons.package, size: 20, color: Colors.blue, ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), ), const SizedBox(height: 4), Wrap( spacing: 6, runSpacing: 4, children: [ if (category?.name != null) _buildSheetBadge(category!.name, const Color(0xFFD4AF37)), if (sku != null && sku.isNotEmpty) _buildSheetBadge('SKU: $sku', Colors.blue), _buildSheetBadge('Unit: $uomUnit', Colors.teal), ], ), ], ), ), ], ), const Divider(height: 18), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( avgPrice > 0 ? 'Avg Price: ₹${avgPrice.toStringAsFixed(2)} / $uomUnit' : 'Price: Not Set', style: TextStyle( fontSize: 13, color: avgPrice > 0 ? (isDark ? Colors.white70 : Colors.grey.shade800) : Colors.orange.shade700, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 3), Text( 'Available Stock: ${availableStock.toStringAsFixed(availableStock.truncateToDouble() == availableStock ? 0 : 2)} $uomUnit', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: availableStock > 0 ? Colors.green.shade700 : Colors.red.shade600, ), ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ if (avgPrice > 0 && availableStock > 0) ...[ Text( '₹${totalStockValue.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.green), ), Text( '₹${unitWithGst.toStringAsFixed(2)}/$uomUnit (incl. ${gstRate.toStringAsFixed(1)}% GST)', style: TextStyle(fontSize: 10, color: Colors.grey.shade500), ), ] else if (avgPrice > 0) ...[ Text( '₹${unitWithGst.toStringAsFixed(2)} / $uomUnit', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: Colors.green), ), Text( 'incl. ${gstRate.toStringAsFixed(1)}% GST', style: TextStyle(fontSize: 10, color: Colors.grey.shade500), ), ] else ...[ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.blue.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.blue.withValues(alpha: 0.3)), ), child: const Text( 'Tap to enter price', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.blue), ), ), ], ], ), ], ), ], ), ), ), ), ); } Widget _buildSearchItemCard({ required Product product, ProductCategory? category, InventoryItem? inventoryItem, required double commodityRate, required bool isCommodity, required String uomUnit, required bool isDark, required VoidCallback onTap, }) { final purity = resolvePurity(categoryPurity: category?.purityFactor, productPurity: product.purityFactor); final unitRate = isCommodity ? (commodityRate > 0 ? (commodityRate * purity) : (inventoryItem?.saleRate ?? product.sellingPrice ?? 0.0)) : (inventoryItem?.saleRate ?? product.sellingPrice ?? 0.0); final weight = inventoryItem?.grossWeight ?? 1.0; final baseAmount = weight * unitRate; final makingCharge = (inventoryItem?.makingCharges != null && inventoryItem!.makingCharges! > 0) ? inventoryItem.makingCharges! : ((product.makingCharges != null && product.makingCharges! > 0) ? product.makingCharges! : (category?.defaultMakingCharge ?? 0.0)); final makingType = (inventoryItem?.makingChargeType != null && inventoryItem!.makingChargeType!.isNotEmpty) ? inventoryItem.makingChargeType! : ((product.makingCharges != null && product.makingCharges! > 0 && product.makingChargesType != null) ? product.makingChargesType! : (category?.makingChargeType ?? 'PER_GRAM')); double makingAmt = 0.0; if (isCommodity) { if (makingType == 'PERCENTAGE') { makingAmt = baseAmount * (makingCharge / 100.0); } else if (makingType == 'PER_PIECE') { makingAmt = makingCharge; } else { makingAmt = weight * makingCharge; } } final gstRate = product.gstRate ?? (category?.defaultGst ?? (isCommodity ? 3.0 : 18.0)); final taxable = baseAmount + makingAmt; final taxAmount = (taxable * gstRate) / 100.0; final totalWithTax = taxable + taxAmount; final huid = inventoryItem?.huid; final sku = inventoryItem?.sku ?? product.sku; return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: isDark ? const Color(0xFF1E293B) : Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: isDark ? Colors.white12 : Colors.grey.shade200), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.04), blurRadius: 8, offset: const Offset(0, 3), ), ], ), child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(16), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: (isCommodity ? (huid != null ? Colors.purple : const Color(0xFFD4AF37)) : Colors.blue).withValues(alpha: 0.12), borderRadius: BorderRadius.circular(10), ), child: Icon( isCommodity ? (huid != null ? LucideIcons.gem : LucideIcons.sparkles) : LucideIcons.tag, size: 20, color: isCommodity ? (huid != null ? Colors.purple : const Color(0xFFD4AF37)) : Colors.blue, ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), ), const SizedBox(height: 4), Wrap( spacing: 6, runSpacing: 4, children: [ if (category?.name != null) _buildSheetBadge(category!.name, const Color(0xFFD4AF37)), if (sku != null && sku.isNotEmpty) _buildSheetBadge('SKU: $sku', Colors.blue), if (isCommodity && huid != null && huid.isNotEmpty) _buildSheetBadge('HUID: $huid', Colors.purple), if (isCommodity) _buildSheetBadge('Purity: ${formatPurity(purity)}', Colors.amber.shade900), if (!isCommodity) _buildSheetBadge('Unit: $uomUnit', Colors.teal), ], ), ], ), ), ], ), const Divider(height: 18), if (isCommodity) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Rate: ₹${unitRate.toStringAsFixed(2)}/$uomUnit • ${weight.toStringAsFixed(3)}$uomUnit', style: TextStyle(fontSize: 13, color: Colors.grey.shade600, fontWeight: FontWeight.w500), ), Text( 'Metal: ₹${baseAmount.toStringAsFixed(2)}', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), ), ], ), const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: Colors.indigo.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.indigo.withValues(alpha: 0.2)), ), child: Text( 'Making: ${makingType == 'PERCENTAGE' ? '$makingCharge%' : (makingType == 'PER_PIECE' ? '₹$makingCharge/pc' : '₹$makingCharge/g')} (₹${makingAmt.toStringAsFixed(2)})', style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.indigo), ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '₹${totalWithTax.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.green), ), Text( 'incl. ${gstRate.toStringAsFixed(1)}% GST', style: TextStyle(fontSize: 10, color: Colors.grey.shade500), ), ], ), ], ), ] else ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( unitRate > 0 ? 'Price: ₹${unitRate.toStringAsFixed(2)} / $uomUnit' : 'Price: Not Set', style: TextStyle( fontSize: 13, color: unitRate > 0 ? (isDark ? Colors.white70 : Colors.grey.shade800) : Colors.orange.shade700, fontWeight: FontWeight.w600, ), ), if (inventoryItem?.grossWeight != null && inventoryItem!.grossWeight! > 0) ...[ const SizedBox(height: 2), Text( 'In Stock: ${(inventoryItem!.grossWeight!).toStringAsFixed((inventoryItem!.grossWeight!).truncateToDouble() == inventoryItem!.grossWeight! ? 0 : 2)} $uomUnit', style: TextStyle(fontSize: 11, color: isDark ? Colors.white54 : Colors.grey.shade600), ), ], ], ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ if (unitRate > 0) ...[ Text( '₹${totalWithTax.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.green), ), Text( 'incl. ${gstRate.toStringAsFixed(1)}% GST', style: TextStyle(fontSize: 10, color: Colors.grey.shade500), ), ] else ...[ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.blue.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.blue.withValues(alpha: 0.3)), ), child: const Text( 'Tap to enter price', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.blue), ), ), ], ], ), ], ), ], ], ), ), ), ), ); } }