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/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 '../../business/providers/business_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'; 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'); DateTime _invoiceDate = DateTime.now(); DateTime? _dueDate; int? _selectedCustomerId; final List _items = []; bool _isLoading = false; 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; _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(); 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 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 isSameState = businessStateId != null && customer?.stateId != null && businessStateId == customer!.stateId; 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 gstRate = item.taxRate > 0 ? item.taxRate : (product?.gstRate ?? 0.0); final weight = item.weight ?? item.quantity; final metalAmount = weight * item.unitPrice; double makingChargeAmt = 0.0; if (item.makingChargesType == 'PERCENTAGE') { makingChargeAmt = metalAmount * (item.makingCharge / 100.0); } else if (item.makingChargesType == 'PER_PIECE') { makingChargeAmt = item.makingCharge; } else { makingChargeAmt = weight * item.makingCharge; } final taxableAmount = metalAmount + 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 += metalAmount; totalMaking += makingChargeAmt; totalTax += taxAmount; processedItems.add(item.copyWith( 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, 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: [ // 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; }); }, ), 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: 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( 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( children: [ Icon(LucideIcons.shoppingBag, 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/HUID to add jewellery', 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), // 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 weight = item.weight ?? item.quantity; final metalAmount = weight * item.unitPrice; double makingChargeAmt = 0.0; if (item.makingChargesType == 'PERCENTAGE') { makingChargeAmt = metalAmount * (item.makingCharge / 100.0); } else if (item.makingChargesType == 'PER_PIECE') { makingChargeAmt = item.makingCharge; } else { makingChargeAmt = weight * item.makingCharge; } final calculatedTotal = item.total > 0 ? item.total : (metalAmount + makingChargeAmt + item.otherCharges - item.discount); 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 (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), ], ), ], ), ), 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), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Metal: ${weight.toStringAsFixed(3)}g × ₹${item.unitPrice.toStringAsFixed(2)}', style: TextStyle(fontSize: 13, color: Colors.grey.shade600), ), Text( '₹${metalAmount.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13), ), ], ), 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), ), ], ), 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( '+ ₹${((metalAmount + makingChargeAmt) * (item.taxRate / 100.0)).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 products = ref.watch(productsProvider).value ?? []; 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 discountAmount = double.tryParse(_discountCtrl.text) ?? 0.0; final isSameState = businessStateId != null && customer?.stateId != null && businessStateId == customer!.stateId; double metalSubtotal = 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 gstRate = item.taxRate > 0 ? item.taxRate : (product?.gstRate ?? 3.0); final weight = item.weight ?? item.quantity; final metalAmt = weight * item.unitPrice; double makingAmt = 0.0; if (item.makingChargesType == 'PERCENTAGE') { makingAmt = metalAmt * (item.makingCharge / 100.0); } else if (item.makingChargesType == 'PER_PIECE') { makingAmt = item.makingCharge; } else { makingAmt = weight * item.makingCharge; } metalSubtotal += metalAmt; makingChargesTotal += makingAmt; rawTaxableTotal += (metalAmt + makingAmt + item.otherCharges); } final effectiveTaxable = (rawTaxableTotal - discountAmount).clamp(0.0, double.infinity); final avgGstRate = _items.isNotEmpty ? (_items.map((i) => i.taxRate > 0 ? i.taxRate : 3.0).reduce((a, b) => a + b) / _items.length) : 3.0; taxTotal = (effectiveTaxable * avgGstRate) / 100.0; final grandTotal = effectiveTaxable + taxTotal; 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), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Metal Subtotal:'), Text('₹${metalSubtotal.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600)), ], ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Making Charges:'), Text('₹${makingChargesTotal.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 (${(taxTotal > 0 ? '1.5%' : '0%')}):', style: TextStyle(color: Colors.grey.shade600)), Text('₹${(taxTotal / 2.0).toStringAsFixed(2)}'), ], ), const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('SGST (${(taxTotal > 0 ? '1.5%' : '0%')}):', style: TextStyle(color: Colors.grey.shade600)), Text('₹${(taxTotal / 2.0).toStringAsFixed(2)}'), ], ), ] else ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('IGST (${(taxTotal > 0 ? '3.0%' : '0%')}):', 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 products = ref.watch(productsProvider).value ?? []; final discountAmount = double.tryParse(_discountCtrl.text) ?? 0.0; double rawTaxable = 0; for (var item in _items) { final weight = item.weight ?? item.quantity; final metalAmt = weight * item.unitPrice; double makingAmt = 0.0; if (item.makingChargesType == 'PERCENTAGE') { makingAmt = metalAmt * (item.makingCharge / 100.0); } else if (item.makingChargesType == 'PER_PIECE') { makingAmt = item.makingCharge; } else { makingAmt = weight * item.makingCharge; } rawTaxable += (metalAmt + makingAmt + item.otherCharges); } final effectiveTaxable = (rawTaxable - discountAmount).clamp(0.0, double.infinity); final avgGst = _items.isNotEmpty ? (_items.map((i) => i.taxRate > 0 ? i.taxRate : 3.0).reduce((a, b) => a + b) / _items.length) : 3.0; final grandTotal = effectiveTaxable + (effectiveTaxable * avgGst / 100.0); // 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!), ), ), ], ), ], ], ), ); } } 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; 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); 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, makingCharges: it.makingCharge, makingChargesType: it.makingChargesType, ); if (it.inventoryItemId != null) { _selectedInventoryItem = invItems.where((i) => i.id == it.inventoryItemId).firstOrNull; } }); }); } } @override void dispose() { _searchCtrl.dispose(); _makingChargeCtrl.dispose(); _discountCtrl.dispose(); super.dispose(); } void _onProductSelected(Product product, ProductCategory? category, double liveCommodityRate) { setState(() { _selectedProduct = product; _selectedInventoryItem = null; 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) { setState(() { _selectedProduct = product; _selectedInventoryItem = item; 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) { if (_selectedProduct == null) return; final weight = _selectedInventoryItem?.grossWeight ?? 1.0; final makingCharge = double.tryParse(_makingChargeCtrl.text) ?? 0.0; final discount = double.tryParse(_discountCtrl.text) ?? 0.0; final metalAmount = weight * unitRate; double makingAmt = 0.0; if (_makingChargeType == 'PERCENTAGE') { makingAmt = metalAmount * (makingCharge / 100.0); } else if (_makingChargeType == 'PER_PIECE') { makingAmt = makingCharge; } else { makingAmt = weight * makingCharge; } final taxable = metalAmount + makingAmt - discount; final gstRate = _selectedProduct?.gstRate ?? (category?.defaultGst ?? 3.0); final taxAmount = (taxable * gstRate) / 100.0; final total = taxable + taxAmount; final item = InvoiceItem( productId: _selectedProduct!.id, inventoryItemId: _selectedInventoryItem?.id, productName: _selectedProduct!.name, categoryName: category?.name, commodityCode: category?.commodityCode, hsnCode: category?.defaultHsn ?? _selectedProduct?.hsnCode, sku: _selectedInventoryItem?.sku ?? _selectedProduct?.sku, huid: _selectedInventoryItem?.huid, description: _selectedProduct!.name, quantity: 1.0, weight: weight, unitPrice: unitRate, makingCharge: makingCharge, makingChargesType: _makingChargeType, 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 isDark = Theme.of(context).brightness == Brightness.dark; ProductCategory? category; if (_selectedProduct?.categoryId != null) { category = categories.where((c) => c.id == _selectedProduct!.categoryId).firstOrNull; } 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 = commodityRate > 0 ? (commodityRate * purityFactor) : (_selectedProduct?.sellingPrice ?? 0.0); final weight = _selectedInventoryItem?.grossWeight ?? 1.0; final metalAmount = weight * effectiveUnitRate; final makingCharge = double.tryParse(_makingChargeCtrl.text) ?? 0.0; double makingAmt = 0.0; if (_makingChargeType == 'PERCENTAGE') { makingAmt = metalAmount * (makingCharge / 100.0); } else if (_makingChargeType == 'PER_PIECE') { makingAmt = makingCharge; } else { makingAmt = weight * makingCharge; } final discount = double.tryParse(_discountCtrl.text) ?? 0.0; final taxable = metalAmount + makingAmt - discount; final gstRate = _selectedProduct?.gstRate ?? (category?.defaultGst ?? 3.0); 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 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: 'Search Product by HUID / 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( query.isEmpty ? 'Available Stock in Inventory (${displayInventoryItems.length}):' : 'Matched Inventory Items (${displayInventoryItems.length}):', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: isDark ? Colors.grey.shade400 : Colors.grey.shade700, ), ), if (query.isEmpty && availableItems.length > 20) Text( 'Showing 20 newest', style: TextStyle(fontSize: 11, color: Colors.grey.shade500, fontStyle: FontStyle.italic), ), ], ), const SizedBox(height: 10), if (displayInventoryItems.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 inventory 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; 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, isDark: isDark, onTap: () => _onInventoryItemSelected(item, p, cat), ); }), ] 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 (_selectedInventoryItem?.huid != null) _buildSheetBadge('HUID: ${_selectedInventoryItem!.huid}', Colors.purple), if (category?.commodityCode != null) _buildSheetBadge('${category!.commodityCode} (${formatPurity(purityFactor)})', Colors.teal), _buildSheetBadge('Purity: ${formatPurity(purityFactor)}', Colors.amber.shade800), ], ), ], ), ), const SizedBox(height: 16), // DISABLED FIELDS: Rounded 12px borders 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: TextFormField( initialValue: '${weight.toStringAsFixed(3)} g', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), enabled: false, decoration: InputDecoration( labelText: 'Weight', prefixIcon: const Icon(LucideIcons.scale, size: 18), contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: TextFormField( key: ValueKey(effectiveUnitRate), initialValue: '₹${effectiveUnitRate.toStringAsFixed(2)}/g', 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), // ENABLED FIELDS: Making Charges & Type 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!), ), ), ], ), 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: 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), 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 _buildSearchItemCard({ required Product product, ProductCategory? category, InventoryItem? inventoryItem, required double commodityRate, required bool isDark, required VoidCallback onTap, }) { final purity = resolvePurity(categoryPurity: category?.purityFactor, productPurity: product.purityFactor); final unitRate = commodityRate > 0 ? (commodityRate * purity) : (product.sellingPrice ?? 0.0); final weight = inventoryItem?.grossWeight ?? 1.0; final metalAmount = 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 (makingType == 'PERCENTAGE') { makingAmt = metalAmount * (makingCharge / 100.0); } else if (makingType == 'PER_PIECE') { makingAmt = makingCharge; } else { makingAmt = weight * makingCharge; } final gstRate = product.gstRate ?? (category?.defaultGst ?? 3.0); final taxable = metalAmount + 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: (huid != null ? Colors.purple : Colors.blue).withValues(alpha: 0.12), borderRadius: BorderRadius.circular(10), ), child: Icon( huid != null ? LucideIcons.gem : LucideIcons.tag, size: 20, color: huid != null ? Colors.purple : 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 (huid != null && huid.isNotEmpty) _buildSheetBadge('HUID: $huid', Colors.purple), _buildSheetBadge('Purity: ${formatPurity(purity)}', Colors.amber.shade900), ], ), ], ), ), ], ), const Divider(height: 18), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Rate: ₹${unitRate.toStringAsFixed(2)}/g • ${weight.toStringAsFixed(3)}g', style: TextStyle(fontSize: 13, color: Colors.grey.shade600, fontWeight: FontWeight.w500), ), Text( 'Metal: ₹${metalAmount.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), ), ], ), ], ), ], ), ), ), ), ); } }