// lib/pages/product_page.dart import 'package:flutter/material.dart'; import 'package:garments_billing_software_offline/database/db_helper.dart'; class ProductPage extends StatefulWidget { const ProductPage({super.key}); @override State createState() => _ProductPageState(); } class _ProductPageState extends State { List> _products = []; // Dropdown data List> _categories = []; List> _brands = []; List> _units = []; List> _gstRates = []; // Controllers final _nameController = TextEditingController(); final _hsnController = TextEditingController(); final _purchaseController = TextEditingController(); final _sellingController = TextEditingController(); final _discountPercentageController = TextEditingController(); final _quantityController = TextEditingController(); // NEW: Quantity // Dropdown selections int? _selectedCategoryId; int? _selectedBrandId; int? _selectedUnitId; int? _selectedGstRateId; @override void initState() { super.initState(); _loadInitialData(); } @override void dispose() { _nameController.dispose(); _hsnController.dispose(); _purchaseController.dispose(); _sellingController.dispose(); _discountPercentageController.dispose(); _quantityController.dispose(); super.dispose(); } // Load data Future _loadInitialData() async { await Future.wait([ _refreshProducts(), _loadDropdowns(), ]); } Future _loadDropdowns() async { _categories = await DatabaseHelper.instance.readAllCategories(); _brands = await DatabaseHelper.instance.readAllBrands(); _units = await DatabaseHelper.instance.readAllUnits(); _gstRates = await DatabaseHelper.instance.readAllGstRates(); setState(() {}); } Future _refreshProducts() async { _products = await DatabaseHelper.instance.readAllProducts(); setState(() {}); } // Reset form void _resetForm() { _nameController.clear(); _hsnController.clear(); _purchaseController.clear(); _sellingController.clear(); _discountPercentageController.clear(); _quantityController.clear(); _selectedCategoryId = null; _selectedBrandId = null; _selectedUnitId = null; _selectedGstRateId = null; setState(() {}); } // Save or Update Product Future _saveProduct([int? id]) async { if (_nameController.text.isEmpty || _hsnController.text.isEmpty || _purchaseController.text.isEmpty || _sellingController.text.isEmpty || _quantityController.text.isEmpty || _selectedCategoryId == null || _selectedBrandId == null || _selectedUnitId == null || _selectedGstRateId == null) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Please fill all required fields.'), backgroundColor: Colors.orange), ); } return; } final purchasePrice = double.tryParse(_purchaseController.text) ?? 0.0; final sellingPrice = double.tryParse(_sellingController.text) ?? 0.0; final discountPercentage = double.tryParse(_discountPercentageController.text) ?? 0.0; final quantity = double.tryParse(_quantityController.text) ?? 0.0; final productData = { 'name': _nameController.text.trim(), 'hsn_code': _hsnController.text.trim(), 'category_id': _selectedCategoryId, 'brand_id': _selectedBrandId, 'unit_id': _selectedUnitId, 'gst_rate_id': _selectedGstRateId, 'purchase_price': purchasePrice, 'selling_price': sellingPrice, 'discount_percentage': discountPercentage > 0 ? discountPercentage : null, 'stock': quantity, // FIXED: Must be 'stock' to match the database column name }; try { if (id == null) { await DatabaseHelper.instance.insertProduct(productData); } else { productData['id'] = id; await DatabaseHelper.instance.updateProduct(productData); } _resetForm(); _refreshProducts(); if (mounted) Navigator.of(context).pop(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${productData['name']} saved successfully!')), ); } } catch (e) { // Print the full error for debugging in the console print('Database Error during Product Save/Update: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Failed to save product. Name might already exist.'), backgroundColor: Colors.red), ); } } } Future _deleteProduct(int id, String name) async { await DatabaseHelper.instance.deleteProduct(id); _refreshProducts(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('$name deleted successfully!')), ); } } // Show View Dialog void _showViewDialog(Map product) { final sellingPrice = product['selling_price'] as double; final discountPercentage = product['discount_percentage'] as double? ?? 0.0; final discountAmount = sellingPrice * (discountPercentage / 100); final quantity = product['stock'] ?? 0.0; showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Product Details: ${product['name']}'), content: SingleChildScrollView( child: SizedBox( width: 400, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildDetailRow('Name', product['name']), _buildDetailRow('HSN Code', product['hsn_code']), const Divider(), _buildDetailRow('Category', product['category_name']), _buildDetailRow('Brand', product['brand_name']), _buildDetailRow('Unit', product['unit_name']), _buildDetailRow('GST Rate', '${product['gst_rate']}%'), const Divider(), _buildDetailRow('Purchase Price', '₹ ${product['purchase_price'].toStringAsFixed(2)}'), _buildDetailRow('Selling Price', '₹ ${sellingPrice.toStringAsFixed(2)}'), _buildDetailRow('Discount %', '$discountPercentage%'), _buildDetailRow('Discount Amount', '₹ ${discountAmount.toStringAsFixed(2)}'), _buildDetailRow('quantity', quantity.toString()), ], ), ), ), actions: [ TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Close')), ], ); }, ); } Widget _buildDetailRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('$label:', style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.grey)), Text(value, style: const TextStyle(fontWeight: FontWeight.w600)), ], ), ); } // Show Add/Edit Dialog void _showEditDialog([Map? product]) { final isEditing = product != null; // We call _resetForm() at the start to ensure controllers are cleared // for a fresh 'Add' or before setting values for 'Edit'. _resetForm(); if (isEditing) { // Update controllers with product data _nameController.text = product!['name']; _hsnController.text = product['hsn_code']; _purchaseController.text = product['purchase_price'].toString(); _sellingController.text = product['selling_price'].toString(); _discountPercentageController.text = (product['discount_percentage'] ?? 0.0).toString(); _quantityController.text = (product['stock'] ?? 0.0).toString(); // Update selected IDs for dropdowns _selectedCategoryId = product['category_id']; _selectedBrandId = product['brand_id']; _selectedUnitId = product['unit_id']; _selectedGstRateId = product['gst_rate_id']; } showDialog( context: context, builder: (context) { // StatefulBuilder is used here to allow the dropdowns to update the dialog state immediately return AlertDialog( title: Text(isEditing ? 'Edit Product' : 'Add New Product'), content: StatefulBuilder( builder: (context, setStateDialog) { return SingleChildScrollView( child: SizedBox( width: 600, child: Column( mainAxisSize: MainAxisSize.min, // Use min size for content children: [ TextField(controller: _nameController, decoration: const InputDecoration(labelText: 'Product Name')), TextField(controller: _hsnController, decoration: const InputDecoration(labelText: 'HSN Code')), const SizedBox(height: 16), // Dropdowns _buildDropdown('Category', _categories, _selectedCategoryId, (v) => setStateDialog(() => _selectedCategoryId = v)), _buildDropdown('Brand', _brands, _selectedBrandId, (v) => setStateDialog(() => _selectedBrandId = v)), _buildDropdown('Unit', _units, _selectedUnitId, (v) => setStateDialog(() => _selectedUnitId = v)), _buildDropdown('GST Rate', _gstRates, _selectedGstRateId, (v) => setStateDialog(() => _selectedGstRateId = v), isGst: true), const SizedBox(height: 16), // Numeric Inputs TextField(controller: _purchaseController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration(labelText: 'Purchase Price')), TextField(controller: _sellingController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration(labelText: 'Selling Price')), TextField(controller: _discountPercentageController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration(labelText: 'Discount (%)')), TextField(controller: _quantityController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration(labelText: 'stock')), ], ), ), ); }, ), actions: [ TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel')), ElevatedButton( onPressed: () => _saveProduct(isEditing ? product!['id'] : null), child: Text(isEditing ? 'Update' : 'Add'), ), ], ); }, ).then((_) => _refreshProducts()); // Refresh list when dialog closes (regardless of save/cancel) } // Helper method for building Dropdowns Widget _buildDropdown(String label, List> items, int? selectedId, Function(int?) onChanged, {bool isGst = false}) { // Determine the list of IDs from the items list to check if selectedId is valid final validIds = items.map((e) => e['id']).toSet(); // Reset selectedId if the current value is not in the list (e.g., if it was deleted) if (selectedId != null && !validIds.contains(selectedId)) { selectedId = null; } return DropdownButtonFormField( decoration: InputDecoration(labelText: label), value: selectedId, items: items.map((item) { // Ensure the value is an int, as the generic type requires it. final itemId = item['id'] as int; return DropdownMenuItem( value: itemId, child: Text(isGst ? '${item['rate']}%' : item['name']), ); }).toList(), onChanged: onChanged, ); } // --- UI Build --- @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Manage Products', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w600, color: Color(0xFF1E3A8A))), const SizedBox(height: 20), Card( elevation: 4, child: Padding( padding: const EdgeInsets.all(20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Expanded(child: Text('Add or manage your products and inventory details.', style: TextStyle(color: Colors.grey))), ElevatedButton.icon( onPressed: () { if (_categories.isEmpty || _brands.isEmpty || _units.isEmpty || _gstRates.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Add at least one Category, Brand, Unit & GST first.'), backgroundColor: Colors.orange)); } else { _showEditDialog(); } }, icon: const Icon(Icons.add), label: const Text('Add Product'), style: ElevatedButton.styleFrom(backgroundColor: Colors.indigo.shade600, foregroundColor: Colors.white), ), ], ), ), ), const SizedBox(height: 20), const Text('Current Products List', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), const SizedBox(height: 10), // 👇 NEW: HEADER ROW FOR THE LIST VIEW Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Row( children: [ // Corresponds to the leading CircleAvatar const SizedBox(width: 40), Expanded( child: Row( children: const [ Expanded(flex: 3, child: Text('Product Name', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blueGrey))), Expanded(flex: 2, child: Text('HSN / Category', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blueGrey))), Expanded(flex: 1, child: Text('GST %', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blueGrey))), Expanded(flex: 1, child: Text('Price (₹)', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blueGrey))), Expanded(flex: 1, child: Text('Stock', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blueGrey))), ], ), ), // Corresponds to the trailing action icons const SizedBox(width: 150), ], ), ), Expanded( child: Card( elevation: 2, child: ListView.builder( itemCount: _products.length, itemBuilder: (context, index) { final p = _products[index]; return Column( children: [ ListTile( leading: CircleAvatar(child: Text('${index + 1}', style: const TextStyle(fontSize: 12))), title: Row( children: [ Expanded(flex: 3, child: Text(p['name'])), Expanded(flex: 2, child: Text('${p['hsn_code']} / ${p['category_name']}', style: const TextStyle(fontSize: 13))), Expanded(flex: 1, child: Text('${p['gst_rate']}%', style: const TextStyle(fontWeight: FontWeight.bold))), Expanded(flex: 1, child: Text('₹ ${p['selling_price'].toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold))), Expanded(flex: 1, child: Text('Qty: ${p['stock']}', style: const TextStyle(color: Colors.green))), ], ), trailing: SizedBox( width: 150, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton(icon: const Icon(Icons.visibility, size: 20, color: Colors.indigo), onPressed: () => _showViewDialog(p)), IconButton(icon: const Icon(Icons.edit, size: 20, color: Colors.blue), onPressed: () => _showEditDialog(p)), IconButton(icon: const Icon(Icons.delete, size: 20, color: Colors.red), onPressed: () => _deleteProduct(p['id'], p['name'])), ], ), ), ), const Divider(height: 1), ], ); }, ), ), ), ], ); } }