import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:intl/intl.dart'; import '../../business/providers/business_provider.dart'; import '../../../core/widgets/barcode_scanner_screen.dart'; import '../../../core/widgets/smart_search_dropdown.dart'; import '../../../core/widgets/premium_text_field.dart'; import '../providers/invoices_provider.dart'; import '../domain/invoice.dart'; import '../providers/customers_provider.dart'; import '../domain/customer.dart'; import '../../inventory/providers/products_provider.dart'; import '../../inventory/domain/product.dart'; import 'add_customer_sheet.dart'; import '../../transactions/providers/providers.dart'; class InvoiceBuilderScreen extends ConsumerStatefulWidget { const InvoiceBuilderScreen({super.key}); @override ConsumerState createState() => _InvoiceBuilderScreenState(); } class _InvoiceBuilderScreenState extends ConsumerState { final _formKey = GlobalKey(); Customer? _selectedCustomer; final List _items = []; bool _isEmi = false; String _emiCycle = 'MONTHLY'; final TextEditingController _emiAmountCtrl = TextEditingController(); final TextEditingController _invoiceNumberCtrl = TextEditingController(text: 'INV-${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}'); final TextEditingController _invoiceDiscountCtrl = TextEditingController(); bool _invoiceDiscountIsPerc = false; DateTime _invoiceDate = DateTime.now(); final TextEditingController _amountPaidCtrl = TextEditingController(); bool _isAmountPaidEdited = false; DateTime? _nextPaymentDate; String _paymentMethod = 'Cash'; int? _selectedWalletId; double get _subtotal => _items.fold(0, (sum, item) => sum + (item.quantity * item.unitPrice)); double get _taxTotal => _items.fold(0, (sum, item) => sum + (item.quantity * item.unitPrice * (item.taxRate / 100))); double get _itemDiscountTotal => _items.fold(0, (sum, item) => sum + item.discount); double get _invoiceDiscountAmount { final raw = double.tryParse(_invoiceDiscountCtrl.text) ?? 0; if (_invoiceDiscountIsPerc) { final taxableAmount = _subtotal + _makingChargeTotal + _otherChargesTotal; return taxableAmount * (raw / 100); } return raw; } double get _discountTotal => _itemDiscountTotal + _invoiceDiscountAmount; double get _makingChargeTotal => _items.fold(0, (sum, item) => sum + item.makingCharge); double get _otherChargesTotal => _items.fold(0, (sum, item) => sum + item.otherCharges); double get _grandTotal => _subtotal + _taxTotal + _makingChargeTotal + _otherChargesTotal - _discountTotal; double get _amountPaid { if (!_isAmountPaidEdited) return _grandTotal; final raw = double.tryParse(_amountPaidCtrl.text) ?? 0; return raw > _grandTotal ? _grandTotal : raw; // Cap at grand total } double get _balanceDue => _grandTotal - _amountPaid; Future _saveInvoice() async { if (!_formKey.currentState!.validate() || _items.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please add items and fill all required fields.'))); return; } final invoice = Invoice( customerId: _selectedCustomer?.id, invoiceNumber: _invoiceNumberCtrl.text, issueDate: _invoiceDate, dueDate: DateTime.now().add(const Duration(days: 30)), subtotal: _subtotal, taxTotal: _taxTotal, discountTotal: _discountTotal, totalAmount: _grandTotal, amountPaid: _amountPaid, paymentMethod: _amountPaid > 0 ? _paymentMethod : null, paymentWalletId: _amountPaid > 0 ? (_selectedWalletId ?? (ref.read(walletProvider).value?.firstOrNull?.id)) : null, nextPaymentDate: _balanceDue > 0 ? (_nextPaymentDate ?? DateTime.now().add(const Duration(days: 30))) : null, isEmi: _isEmi, emiAmount: _isEmi && _emiAmountCtrl.text.isNotEmpty ? double.parse(_emiAmountCtrl.text) : null, emiCycle: _isEmi ? _emiCycle : null, emiStartDate: _isEmi ? DateTime.now().add(const Duration(days: 30)) : null, items: _items, ); try { await ref.read(invoicesProvider.notifier).createInvoice(invoice); if (mounted) { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Invoice Created!'))); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e'))); } } } void _showAddItemDialog() { Product? selectedProduct; final TextEditingController descCtrl = TextEditingController(); final TextEditingController qtyCtrl = TextEditingController(text: '1'); final TextEditingController priceCtrl = TextEditingController(); final TextEditingController taxCtrl = TextEditingController(text: '0'); final TextEditingController discountCtrl = TextEditingController(text: '0'); final TextEditingController makingCtrl = TextEditingController(text: '0'); final TextEditingController otherCtrl = TextEditingController(text: '0'); String uomStr = ''; showDialog( context: context, builder: (context) { bool isDiscPerc = false; return StatefulBuilder( builder: (context, setDialogState) { return AlertDialog( title: const Text('Add Line Item'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Consumer( builder: (context, dialogRef, _) { final productsState = dialogRef.watch(productsProvider); return productsState.when( data: (products) => Row( children: [ Expanded( child: SmartSearchDropdown( hintText: 'Select Product (Optional)', value: selectedProduct, items: products, itemAsString: (p) => '${p.name} ${p.sku != null && p.sku!.isNotEmpty ? "(${p.sku})" : ""} (₹${p.sellingPrice})', onChanged: (p) { setDialogState(() { selectedProduct = p; if (p != null) { descCtrl.text = p.name; priceCtrl.text = p.sellingPrice?.toString() ?? '0'; taxCtrl.text = p.gstRate?.toString() ?? '0'; makingCtrl.text = p.makingCharges?.toString() ?? '0'; uomStr = ''; } }); } ), ), const SizedBox(width: 8), IconButton( onPressed: () async { final String? code = await Navigator.push( context, MaterialPageRoute(builder: (_) => const BarcodeScannerScreen()), ); if (code != null && code.isNotEmpty) { final source = ref.read(businessFeatureProvider).value?.barcodeSource ?? 'SKU'; Product? matched; if (source == 'BARCODE') { matched = products.cast().firstWhere((p) => p?.barcode == code, orElse: () => null); } else { matched = products.cast().firstWhere((p) => p?.sku == code, orElse: () => null); } if (matched != null) { setDialogState(() { selectedProduct = matched; descCtrl.text = matched!.name; priceCtrl.text = matched!.sellingPrice?.toString() ?? '0'; taxCtrl.text = matched!.gstRate?.toString() ?? '0'; makingCtrl.text = matched!.makingCharges?.toString() ?? '0'; }); } else { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('No product found for $source: $code'))); } } } }, icon: const Icon(LucideIcons.scanLine), color: Theme.of(context).colorScheme.primary, tooltip: 'Scan Barcode', ) ], ), loading: () => const Center(child: CircularProgressIndicator()), error: (e, stack) => Text('Error loading products: $e'), ); } ), const SizedBox(height: 12), PremiumTextField(controller: descCtrl, labelText: 'Description'), const SizedBox(height: 12), Row( children: [ Expanded(child: PremiumTextField(controller: qtyCtrl, labelText: uomStr.isNotEmpty ? 'Qty ($uomStr)' : 'Quantity', keyboardType: TextInputType.number)), const SizedBox(width: 8), Expanded(child: PremiumTextField(controller: priceCtrl, labelText: 'Unit Price', keyboardType: TextInputType.number)), ], ), const SizedBox(height: 12), Row( children: [ Expanded(child: PremiumTextField(controller: taxCtrl, labelText: 'Tax %', keyboardType: TextInputType.number)), const SizedBox(width: 8), Expanded( child: PremiumTextField( controller: discountCtrl, labelText: isDiscPerc ? 'Discount %' : 'Discount (₹)', keyboardType: TextInputType.number, suffixIcon: IconButton( icon: Icon(isDiscPerc ? LucideIcons.percent : LucideIcons.indianRupee, size: 16), onPressed: () => setDialogState(() => isDiscPerc = !isDiscPerc), ), ) ), ], ), const SizedBox(height: 12), Row( children: [ Expanded(child: PremiumTextField(controller: makingCtrl, labelText: 'Making Chg', keyboardType: TextInputType.number)), const SizedBox(width: 8), Expanded(child: PremiumTextField(controller: otherCtrl, labelText: 'Other Chg', keyboardType: TextInputType.number)), ], ), ], ), ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), TextButton( onPressed: () { final qty = double.tryParse(qtyCtrl.text) ?? 1; final price = double.tryParse(priceCtrl.text) ?? 0; final tax = double.tryParse(taxCtrl.text) ?? 0; final discRaw = double.tryParse(discountCtrl.text) ?? 0; final disc = isDiscPerc ? ((qty * price) * (discRaw / 100)) : discRaw; final making = double.tryParse(makingCtrl.text) ?? 0; final other = double.tryParse(otherCtrl.text) ?? 0; final total = (qty * price) + (qty * price * (tax / 100)) + making + other - disc; setState(() { _items.add(InvoiceItem( productId: selectedProduct?.id, sku: selectedProduct?.sku, description: descCtrl.text, quantity: qty, unitPrice: price, taxRate: tax, discount: disc, makingCharge: making, otherCharges: other, total: total, )); }); Navigator.pop(context); }, child: const Text('Add Item'), ), ], ); }, ); }, ); } void _scanAndAddBarcode() async { final barcode = await Navigator.push( context, MaterialPageRoute(builder: (_) => const BarcodeScannerScreen()), ); if (barcode != null && barcode.isNotEmpty) { final products = ref.read(productsProvider).value ?? []; final businessFeature = ref.read(businessFeatureProvider).value; final bool useBarcodeField = businessFeature?.barcodeSource == 'BARCODE'; final p = products.where((p) { if (useBarcodeField) { return p.barcode?.toLowerCase() == barcode.toLowerCase(); } else { return p.sku?.toLowerCase() == barcode.toLowerCase(); } }).firstOrNull; if (p != null) { setState(() { final existingIndex = _items.indexWhere((item) => item.productId == p.id); if (existingIndex >= 0) { // Increment qty final item = _items[existingIndex]; final newQty = item.quantity + 1; final newTotal = (newQty * item.unitPrice) + (newQty * item.unitPrice * (item.taxRate / 100)) + item.makingCharge + item.otherCharges - item.discount; _items[existingIndex] = item.copyWith(quantity: newQty, total: newTotal); } else { // Add new final price = p.sellingPrice ?? 0; final tax = p.gstRate ?? 0; final making = p.makingCharges ?? 0; final total = price + (price * (tax / 100)) + making; _items.add(InvoiceItem( productId: p.id, sku: p.sku, description: p.name, quantity: 1, unitPrice: price, taxRate: tax, makingCharge: making, otherCharges: 0, discount: 0, total: total, )); } }); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added ${p.name} to invoice'))); } } else { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Product not found for barcode: $barcode'))); } } } } @override Widget build(BuildContext context) { final customersState = ref.watch(customersProvider); final formatCurrency = NumberFormat.currency(symbol: '₹'); return Scaffold( backgroundColor: Colors.grey[50], appBar: AppBar( title: const Text('Create Invoice'), elevation: 0, backgroundColor: Colors.white, foregroundColor: Colors.black, actions: [ TextButton( onPressed: _saveInvoice, child: const Text('Save Invoice', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue)), ) ], ), body: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: Form( key: _formKey, child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Customer Selection Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Customer', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), TextButton.icon( onPressed: () { showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => const AddCustomerSheet(), ); }, icon: const Icon(LucideIcons.plus, size: 16), label: const Text('Add Customer'), ), ], ), const SizedBox(height: 8), customersState.when( loading: () => const CircularProgressIndicator(), error: (err, stack) => Text('Error loading customers: $err'), data: (customers) => SmartSearchDropdown( hintText: 'Search a Customer...', value: _selectedCustomer, items: customers, itemAsString: (c) => c.name, onChanged: (val) => setState(() => _selectedCustomer = val), ), ), ], ), ), const SizedBox(height: 16), // Invoice Details Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Invoice Details', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), const SizedBox(height: 12), PremiumTextField( controller: _invoiceNumberCtrl, labelText: 'Invoice Number', validator: (val) => val == null || val.isEmpty ? 'Required' : null, ), const SizedBox(height: 12), ListTile( contentPadding: EdgeInsets.zero, title: const Text('Invoice Date'), subtitle: Text(DateFormat('yyyy-MM-dd').format(_invoiceDate)), trailing: const Icon(LucideIcons.calendar), onTap: () async { final dt = await showDatePicker( context: context, initialDate: _invoiceDate, firstDate: DateTime(2000), lastDate: DateTime(2100), ); if (dt != null) { setState(() => _invoiceDate = dt); } }, ), const SizedBox(height: 12), ], ), ), const SizedBox(height: 16), // Items Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Line Items', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), Row( children: [ Consumer( builder: (context, ref, child) { final feature = ref.watch(businessFeatureProvider).value; final isBarcode = feature?.barcodeSource == 'BARCODE'; return Row( children: [ Text('Scan by:', style: TextStyle(fontSize: 12, color: Colors.grey.shade600)), const SizedBox(width: 4), Text('SKU', style: TextStyle(fontSize: 10, fontWeight: !isBarcode ? FontWeight.bold : FontWeight.normal, color: !isBarcode ? Colors.blue : Colors.grey)), Transform.scale( scale: 0.7, child: Switch( value: isBarcode, activeColor: Colors.blue, onChanged: (val) { if (feature != null) { ref.read(businessFeatureProvider.notifier).updateFeatures( feature.copyWith(barcodeSource: val ? 'BARCODE' : 'SKU') ); } }, ), ), Text('EAN', style: TextStyle(fontSize: 10, fontWeight: isBarcode ? FontWeight.bold : FontWeight.normal, color: isBarcode ? Colors.blue : Colors.grey)), ], ); }, ), IconButton( icon: const Icon(LucideIcons.scanLine, color: Colors.blue), onPressed: _scanAndAddBarcode, tooltip: 'Scan to add', ), TextButton.icon( onPressed: _showAddItemDialog, icon: const Icon(LucideIcons.plus), label: const Text('Add'), ), ], ) ], ), if (_items.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 16.0), child: Center(child: Text('No items added', style: TextStyle(color: Colors.grey))), ), for (var i = 0; i < _items.length; i++) Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey[50], border: Border.all(color: Colors.grey[200]!), borderRadius: BorderRadius.circular(12) ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_items[i].description ?? 'Item ${i+1}', style: const TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 4), Wrap( spacing: 8, runSpacing: 4, children: [ if (_items[i].sku != null && _items[i].sku!.isNotEmpty) Text('SKU: ${_items[i].sku}', style: const TextStyle(fontSize: 12, color: Colors.grey)), Text('Qty: ${_items[i].quantity}', style: const TextStyle(fontSize: 12, color: Colors.grey)), Text('Rate: ${formatCurrency.format(_items[i].unitPrice)}', style: const TextStyle(fontSize: 12, color: Colors.grey)), if (_items[i].taxRate > 0) Text('Tax: ${_items[i].taxRate}%', style: const TextStyle(fontSize: 12, color: Colors.grey)), if (_items[i].makingCharge > 0) Text('Making: ${formatCurrency.format(_items[i].makingCharge)}', style: const TextStyle(fontSize: 12, color: Colors.grey)), if (_items[i].otherCharges > 0) Text('Other: ${formatCurrency.format(_items[i].otherCharges)}', style: const TextStyle(fontSize: 12, color: Colors.grey)), if (_items[i].discount > 0) Text('Disc: -${formatCurrency.format(_items[i].discount)}', style: const TextStyle(fontSize: 12, color: Colors.red)), ], ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(formatCurrency.format(_items[i].total), style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.blue)), IconButton( padding: EdgeInsets.zero, constraints: const BoxConstraints(), icon: const Icon(LucideIcons.trash2, color: Colors.red, size: 18), onPressed: () => setState(() => _items.removeAt(i)), ), ], ) ], ), ), const Divider(), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Invoice Discount', style: TextStyle(fontWeight: FontWeight.w600)), SizedBox( width: 150, child: TextField( controller: _invoiceDiscountCtrl, textAlign: TextAlign.right, decoration: InputDecoration( hintText: '0.00', isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), prefixIcon: IconButton( icon: Icon(_invoiceDiscountIsPerc ? LucideIcons.percent : LucideIcons.indianRupee, size: 16), onPressed: () => setState(() => _invoiceDiscountIsPerc = !_invoiceDiscountIsPerc), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), onChanged: (val) { setState(() { if (!_isAmountPaidEdited) { _amountPaidCtrl.text = _grandTotal.toStringAsFixed(2); } }); }, ), ), ], ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Total Amount', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), Text(formatCurrency.format(_grandTotal), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.blue)), ], ), const Divider(height: 32), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Amount Paid Now', style: TextStyle(fontWeight: FontWeight.w600)), SizedBox( width: 150, child: TextField( controller: _amountPaidCtrl, textAlign: TextAlign.right, decoration: InputDecoration( hintText: _grandTotal.toStringAsFixed(2), isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), onChanged: (val) => setState(() { _isAmountPaidEdited = true; }), ), ), ], ), if (_amountPaid > 0) ...[ const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Receive Into Wallet', style: TextStyle(fontWeight: FontWeight.w600)), SizedBox( width: 150, child: Consumer( builder: (context, consumerRef, _) { final walletsState = consumerRef.watch(walletProvider); final allWallets = walletsState.value ?? []; final wallets = allWallets.where((w) { if (_paymentMethod == 'Cash') { return w.nature == 'CASH'; } else { return w.nature == 'INCOME' || w.nature == 'SAVINGS'; } }).toList(); if (wallets.isNotEmpty && (_selectedWalletId == null || !wallets.any((w) => w.id == _selectedWalletId))) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) setState(() => _selectedWalletId = wallets.first.id); }); } return DropdownButtonHideUnderline( child: DropdownButton( value: _selectedWalletId, isDense: true, hint: const Text('Wallet'), items: wallets .map((w) => DropdownMenuItem(value: w.id, child: Text(w.name))) .toList(), onChanged: (val) => setState(() => _selectedWalletId = val), ), ); }, ), ), ], ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Payment Method', style: TextStyle(fontWeight: FontWeight.w600)), SizedBox( width: 150, child: DropdownButtonHideUnderline( child: DropdownButton( value: _paymentMethod, isDense: true, items: ['Cash', 'UPI', 'Bank Transfer', 'Card'] .map((m) => DropdownMenuItem(value: m, child: Text(m))) .toList(), onChanged: (val) { if (val != null) { setState(() { _paymentMethod = val; _selectedWalletId = null; }); } }, ), ), ), ], ), ], const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Balance Due', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red)), Text(formatCurrency.format(_balanceDue), style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.red)), ], ), if (_balanceDue > 0) ...[ const SizedBox(height: 12), SwitchListTile( title: const Text('Enable EMI / Installments'), value: _isEmi, onChanged: (val) => setState(() => _isEmi = val), contentPadding: EdgeInsets.zero, ), if (_isEmi) ...[ Row( children: [ Expanded( child: PremiumTextField( controller: _emiAmountCtrl, labelText: 'EMI Amount', keyboardType: TextInputType.number, ), ), const SizedBox(width: 8), Expanded( child: SmartSearchDropdown( hintText: 'Cycle', value: _emiCycle, items: const ['MONTHLY', 'WEEKLY'], itemAsString: (val) => val == 'MONTHLY' ? 'Monthly' : 'Weekly', onChanged: (val) => setState(() => _emiCycle = val!), ), ), ], ) ] else ...[ const SizedBox(height: 12), ListTile( contentPadding: EdgeInsets.zero, title: const Text('Next Payment Date', style: TextStyle(fontWeight: FontWeight.w600)), subtitle: Text(_nextPaymentDate == null ? 'Not Selected' : DateFormat('yyyy-MM-dd').format(_nextPaymentDate!)), trailing: const Icon(LucideIcons.calendar), onTap: () async { final dt = await showDatePicker( context: context, initialDate: _nextPaymentDate ?? DateTime.now().add(const Duration(days: 30)), firstDate: DateTime.now(), lastDate: DateTime(2100), ); if (dt != null) { setState(() => _nextPaymentDate = dt); } }, ), ], ], ], ), ), const SizedBox(height: 32), ], ), ), ), ), ); } }