first commit
This commit is contained in:
724
lib/Screens/Products/add product/add_edit_comboItem.dart
Normal file
724
lib/Screens/Products/add product/add_edit_comboItem.dart
Normal file
@@ -0,0 +1,724 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_model.dart';
|
||||
import 'package:mobile_pos/core/theme/_app_colors.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import '../../../Provider/product_provider.dart';
|
||||
import '../../../constant.dart';
|
||||
import 'combo_product_form.dart';
|
||||
|
||||
class AddOrEditComboItem extends ConsumerStatefulWidget {
|
||||
final ComboItem? existingItem;
|
||||
final Function(ComboItem) onSubmit;
|
||||
|
||||
const AddOrEditComboItem({
|
||||
super.key,
|
||||
this.existingItem,
|
||||
required this.onSubmit,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<AddOrEditComboItem> createState() => _AddOrEditComboItemPopupState();
|
||||
}
|
||||
|
||||
class _AddOrEditComboItemPopupState extends ConsumerState<AddOrEditComboItem> {
|
||||
Product? selectedProduct;
|
||||
Stock? selectedStock;
|
||||
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
final TextEditingController qtyController = TextEditingController();
|
||||
final TextEditingController unitController = TextEditingController();
|
||||
final TextEditingController priceController = TextEditingController();
|
||||
final TextEditingController totalController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.existingItem != null) {
|
||||
final item = widget.existingItem!;
|
||||
selectedProduct = item.product;
|
||||
selectedStock = item.stockData;
|
||||
|
||||
if (item.product.productType == 'variant' && selectedStock != null) {
|
||||
searchController.text = "${item.product.productName} - ${selectedStock?.variantName}";
|
||||
} else {
|
||||
searchController.text = item.product.productName ?? '';
|
||||
}
|
||||
|
||||
qtyController.text = item.quantity.toString();
|
||||
unitController.text = item.product.unit?.unitName ?? 'Pcs';
|
||||
|
||||
priceController.text = (item.manualPurchasePrice ?? selectedStock?.productPurchasePrice ?? 0).toString();
|
||||
|
||||
_calculateTotal();
|
||||
}
|
||||
|
||||
// if (widget.existingItem != null) {
|
||||
// // Load existing data for Edit Mode
|
||||
// final item = widget.existingItem!;
|
||||
// selectedProduct = item.product;
|
||||
// selectedStock = item.stockData;
|
||||
// searchController.text = item.product.productName ?? '';
|
||||
// qtyController.text = item.quantity.toString();
|
||||
// unitController.text = item.product.unit?.unitName ?? 'Pcs';
|
||||
// priceController.text = item.purchasePrice.toString();
|
||||
// _calculateTotal();
|
||||
// } else {
|
||||
// // Add Mode Defaults
|
||||
// qtyController.text = '1';
|
||||
// unitController.text = 'Pcs';
|
||||
// }
|
||||
}
|
||||
|
||||
void _calculateTotal() {
|
||||
double qty = double.tryParse(qtyController.text) ?? 0;
|
||||
double price = double.tryParse(priceController.text) ?? 0;
|
||||
totalController.text = (qty * price).toStringAsFixed(2);
|
||||
}
|
||||
|
||||
late var _searchController = TextEditingController();
|
||||
// Product? selectedCustomer;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final productListAsync = ref.watch(productProvider);
|
||||
final _theme = Theme.of(context);
|
||||
final _lang = l.S.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
widget.existingItem == null ? _lang.addProduct : _lang.editProduct,
|
||||
style: _theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
centerTitle: false,
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 22,
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1.0),
|
||||
child: Divider(height: 1, thickness: 1, color: kBottomBorder),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.existingItem == null) ...[
|
||||
// --------------use typehead---------------------
|
||||
productListAsync.when(
|
||||
data: (products) {
|
||||
// Filter out combos
|
||||
final filteredProducts = products.where((p) => p.productType != 'combo').toList();
|
||||
|
||||
return TypeAheadField<Map<String, dynamic>>(
|
||||
emptyBuilder: (context) => Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(_lang.noItemFound),
|
||||
),
|
||||
builder: (context, controller, focusNode) {
|
||||
_searchController = controller;
|
||||
return TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(AntDesign.search_outline, color: kGreyTextColor),
|
||||
hintText: selectedProduct != null ? selectedProduct?.productName : _lang.searchProduct,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
controller.clear();
|
||||
selectedProduct = null;
|
||||
selectedStock = null;
|
||||
setState(() {});
|
||||
},
|
||||
icon: Icon(Icons.close, color: kSubPeraColor),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
suggestionsCallback: (pattern) {
|
||||
final query = pattern.toLowerCase().trim();
|
||||
final List<Map<String, dynamic>> suggestions = [];
|
||||
|
||||
for (var product in filteredProducts) {
|
||||
// Skip combo products (already filtered above)
|
||||
if (product.productType != 'variant') {
|
||||
final productName = (product.productName ?? '').toLowerCase();
|
||||
if (query.isEmpty || productName.contains(query)) {
|
||||
suggestions.add({'type': 'single', 'product': product});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Variant product
|
||||
bool headerAdded = false;
|
||||
final parentName = (product.productName ?? '').toLowerCase();
|
||||
|
||||
for (var s in product.stocks ?? []) {
|
||||
final variantName = (s.variantName ?? '').toLowerCase();
|
||||
|
||||
// Combine parent name + variant name for searching
|
||||
final combinedName = '$parentName $variantName';
|
||||
|
||||
if (query.isEmpty || combinedName.contains(query)) {
|
||||
if (!headerAdded) {
|
||||
suggestions.add({'type': 'header', 'product': product});
|
||||
headerAdded = true;
|
||||
}
|
||||
suggestions.add({
|
||||
'type': 'variant',
|
||||
'product': product,
|
||||
'stock': s,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
},
|
||||
// suggestionsCallback: (pattern) {
|
||||
// final query = pattern.toLowerCase().trim();
|
||||
// final List<Map<String, dynamic>> suggestions = [];
|
||||
//
|
||||
// for (var product in filteredProducts) {
|
||||
// if (product.productType != 'variant') {
|
||||
// // Single product is selectable
|
||||
// final productName = (product.productName ?? '').toLowerCase();
|
||||
// if (query.isEmpty || productName.contains(query)) {
|
||||
// suggestions.add({'type': 'single', 'product': product});
|
||||
// }
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// // Variant parent is only a header
|
||||
// bool headerAdded = false;
|
||||
//
|
||||
// // Check if parent name matches
|
||||
// final productName = (product.productName ?? '').toLowerCase();
|
||||
// if (query.isEmpty || productName.contains(query)) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
// }
|
||||
//
|
||||
// // Check variant names
|
||||
// for (var s in product.stocks ?? []) {
|
||||
// final variantName = (s.variantName ?? '').toLowerCase();
|
||||
// if (query.isEmpty || variantName.contains(query)) {
|
||||
// if (!headerAdded) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
// }
|
||||
// suggestions.add({
|
||||
// 'type': 'variant',
|
||||
// 'product': product,
|
||||
// 'stock': s,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return suggestions;
|
||||
// },
|
||||
itemBuilder: (context, suggestion) {
|
||||
final type = suggestion['type'] as String;
|
||||
|
||||
if (type == 'header') {
|
||||
final p = suggestion['product'] as Product;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
// Just close the suggestion box without selecting anything
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
leading: Icon(Icons.circle, color: Colors.black, size: 10),
|
||||
title: Text(
|
||||
p.productName ?? '',
|
||||
style: _theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (type == 'variant') {
|
||||
final product = suggestion['product'] as Product;
|
||||
final stock = suggestion['stock'] as Stock;
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
leading: Icon(Icons.subdirectory_arrow_right, color: Colors.grey, size: 18),
|
||||
title: Text("${product.productName} (${stock.variantName ?? 'n/a'})"),
|
||||
subtitle: Text(
|
||||
'${_lang.stock}: ${stock.productStock}, ${_lang.price}: $currency${stock.productPurchasePrice}, ${_lang.batch}: ${stock.batchNo}'),
|
||||
);
|
||||
}
|
||||
|
||||
// single product
|
||||
final product = suggestion['product'] as Product;
|
||||
return ListTile(
|
||||
title: Text(product.productName ?? ''),
|
||||
subtitle: Text(
|
||||
'${_lang.stock}: ${product.stocksSumProductStock ?? 0}, ${_lang.price}: $currency${product.productPurchasePrice}'),
|
||||
);
|
||||
},
|
||||
onSelected: (suggestion) {
|
||||
final type = suggestion['type'] as String;
|
||||
|
||||
if (type == 'variant' || type == 'single') {
|
||||
final product = suggestion['product'] as Product;
|
||||
|
||||
setState(() {
|
||||
selectedProduct = product;
|
||||
|
||||
if (type == 'variant') {
|
||||
selectedStock = suggestion['stock'] as Stock;
|
||||
} else {
|
||||
selectedStock = product.stocks?.isNotEmpty == true ? product.stocks!.first : null;
|
||||
}
|
||||
|
||||
_searchController.text = type == 'variant'
|
||||
? "${product.productName} - ${selectedStock?.variantName}"
|
||||
: product.productName ?? '';
|
||||
|
||||
unitController.text = product.unit?.unitName ?? 'Pcs';
|
||||
priceController.text = (selectedStock?.productPurchasePrice ?? 0).toStringAsFixed(2);
|
||||
|
||||
_calculateTotal();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => LinearProgressIndicator(),
|
||||
error: (e, _) => Text("Error: $e"),
|
||||
),
|
||||
// productListAsync.when(
|
||||
// data: (products) {
|
||||
// final List<Product> filteredProducts = products.where((p) => p.productType != 'combo').toList();
|
||||
//
|
||||
// return TypeAheadField<Map<String, dynamic>>(
|
||||
// emptyBuilder: (context) => Padding(
|
||||
// padding: const EdgeInsets.all(12),
|
||||
// child: Text("No item found"),
|
||||
// ),
|
||||
// builder: (context, controller, focusNode) {
|
||||
// _searchController = controller;
|
||||
// return TextField(
|
||||
// controller: controller,
|
||||
// focusNode: focusNode,
|
||||
// decoration: InputDecoration(
|
||||
// prefixIcon: Icon(AntDesign.search_outline, color: kGreyTextColor),
|
||||
// hintText: selectedProduct != null ? selectedProduct?.productName : 'Search product',
|
||||
// suffixIcon: IconButton(
|
||||
// onPressed: () {
|
||||
// controller.clear();
|
||||
// selectedProduct = null;
|
||||
// selectedStock = null;
|
||||
// setState(() {});
|
||||
// },
|
||||
// icon: Icon(Icons.close, color: kSubPeraColor),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// suggestionsCallback: (pattern) {
|
||||
// final query = pattern.toLowerCase().trim();
|
||||
// final List<Map<String, dynamic>> suggestions = [];
|
||||
//
|
||||
// for (var product in filteredProducts) {
|
||||
// final productName = (product.productName ?? '').toLowerCase();
|
||||
// if (product.productType != 'variant') {
|
||||
// if (query.isEmpty || productName.contains(query)) {
|
||||
// suggestions.add({'type': 'single', 'product': product});
|
||||
// }
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// bool headerAdded = false;
|
||||
//
|
||||
// if (query.isEmpty) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
//
|
||||
// for (var s in product.stocks ?? []) {
|
||||
// suggestions.add({
|
||||
// 'type': 'variant',
|
||||
// 'product': product,
|
||||
// 'stock': s,
|
||||
// });
|
||||
// }
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// if (productName.contains(query)) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
// }
|
||||
//
|
||||
// for (var s in product.stocks ?? []) {
|
||||
// final variantName = (s.variantName ?? '').toLowerCase();
|
||||
//
|
||||
// if (variantName.contains(query)) {
|
||||
// if (!headerAdded) {
|
||||
// // Only add header once
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
// }
|
||||
//
|
||||
// suggestions.add({
|
||||
// 'type': 'variant',
|
||||
// 'product': product,
|
||||
// 'stock': s,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return suggestions;
|
||||
// },
|
||||
// itemBuilder: (context, suggestion) {
|
||||
// final type = suggestion['type'] as String;
|
||||
// if (type == 'header') {
|
||||
// final p = suggestion['product'] as Product;
|
||||
// return ListTile(
|
||||
// contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// leading: Icon(Icons.circle, color: Colors.black, size: 10),
|
||||
// title: Text(
|
||||
// p.productName ?? '',
|
||||
// style: _theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
// // header is not selectable, so we make it visually disabled
|
||||
// enabled: false,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// if (type == 'variant') {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// final stock = suggestion['stock'] as Stock;
|
||||
// return ListTile(
|
||||
// contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// leading: Icon(Icons.subdirectory_arrow_right, color: Colors.grey, size: 18),
|
||||
// title: Text("${product.productName} (${stock.variantName ?? 'n/a'})"),
|
||||
// subtitle: Text('Stock: ${stock.productStock}, Price: $currency${stock.productPurchasePrice}, Batch: ${stock.batchNo}'),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// // single product
|
||||
// final product = suggestion['product'] as Product;
|
||||
// return ListTile(
|
||||
// title: Text(product.productName ?? ''),
|
||||
// subtitle: Text('Stock: ${product.stocksSumProductStock ?? 0}, Price: $currency${product.productPurchasePrice}'),
|
||||
// );
|
||||
// },
|
||||
// onSelected: (suggestion) {
|
||||
// final type = suggestion['type'] as String;
|
||||
// // Only allow single or variant selection
|
||||
// if (type == 'single' || type == 'variant') {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// setState(() {
|
||||
// selectedProduct = product;
|
||||
//
|
||||
// if (type == 'variant') {
|
||||
// selectedStock = suggestion['stock'] as Stock;
|
||||
// } else {
|
||||
// selectedStock = product.stocks?.isNotEmpty == true ? product.stocks!.first : null;
|
||||
// }
|
||||
//
|
||||
// // Update search field
|
||||
// _searchController.text = type == 'variant' ? "${product.productName} - ${selectedStock?.variantName}" : product.productName ?? '';
|
||||
//
|
||||
// // Update unit field
|
||||
// unitController.text = product.unit?.unitName ?? 'Pcs';
|
||||
//
|
||||
// // Update price field
|
||||
// priceController.text = (selectedStock?.productPurchasePrice ?? 0).toStringAsFixed(2);
|
||||
//
|
||||
// // Recalculate total
|
||||
// _calculateTotal();
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// loading: () => LinearProgressIndicator(),
|
||||
// error: (e, _) => Text("Error: $e"),
|
||||
// ),
|
||||
// --------------use typehead---------------------
|
||||
] else ...[
|
||||
TextFormField(
|
||||
controller: searchController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.product,
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Color(0xFFF5F5F5),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// --------previous code-----------------
|
||||
// if (widget.existingItem == null) ...[
|
||||
// // --------------use typehead---------------------
|
||||
// productListAsync.when(
|
||||
// data: (products) {
|
||||
// // Filter out combo products
|
||||
// final filteredProducts = products.where((p) => p.productType != 'combo').toList();
|
||||
//
|
||||
// return TypeAheadField<Map<String, dynamic>>(
|
||||
// builder: (context, controller, focusNode) {
|
||||
// return TextField(
|
||||
// controller: _searchController,
|
||||
// focusNode: focusNode,
|
||||
// decoration: InputDecoration(
|
||||
// prefixIcon: Icon(AntDesign.search_outline, color: kGreyTextColor),
|
||||
// hintText: selectedProduct != null ? selectedProduct?.productName : 'Search product',
|
||||
// suffixIcon: IconButton(
|
||||
// onPressed: () {
|
||||
// _searchController.clear();
|
||||
// selectedProduct = null;
|
||||
// setState(() {});
|
||||
// },
|
||||
// icon: Icon(Icons.close, color: kSubPeraColor),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// suggestionsCallback: (pattern) {
|
||||
// final List<Map<String, dynamic>> suggestions = [];
|
||||
//
|
||||
// for (var product in filteredProducts) {
|
||||
// if (product.productType == 'variant') {
|
||||
// // Show parent product as a header if it matches the search
|
||||
// if ((product.productName ?? '').toLowerCase().contains(pattern.toLowerCase())) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// }
|
||||
//
|
||||
// // Show variant stocks
|
||||
// for (var stock in product.stocks ?? []) {
|
||||
// if ((stock.variantName ?? '').toLowerCase().contains(pattern.toLowerCase())) {
|
||||
// suggestions.add({'type': 'variant', 'product': product, 'stock': stock});
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // Single product
|
||||
// if ((product.productName ?? '').toLowerCase().contains(pattern.toLowerCase())) {
|
||||
// suggestions.add({'type': 'single', 'product': product});
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return suggestions;
|
||||
// },
|
||||
// itemBuilder: (context, suggestion) {
|
||||
// final type = suggestion['type'] as String;
|
||||
// if (type == 'header') {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// return ListTile(
|
||||
// contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// leading: Icon(
|
||||
// Icons.circle,
|
||||
// color: Colors.black,
|
||||
// size: 10,
|
||||
// ),
|
||||
// title: Text(
|
||||
// product.productName ?? '',
|
||||
// style: _theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
// );
|
||||
// } else if (type == 'variant') {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// final stock = suggestion['stock'] as Stock;
|
||||
// return ListTile(
|
||||
// contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// leading: Icon(Icons.subdirectory_arrow_right, color: Colors.grey, size: 18),
|
||||
// title: Text("${product.productName} (${stock.variantName ?? 'n/a'})"),
|
||||
// subtitle: Text('Stock: ${stock.productStock}, Price: $currency${stock.productPurchasePrice}, Batch: ${stock.batchNo}'),
|
||||
// );
|
||||
// } else {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// return ListTile(
|
||||
// title: Text(product.productName ?? ''),
|
||||
// subtitle: Text('Stock: ${product.stocksSumProductStock ?? 0}, Price: $currency${product.productPurchasePrice}'),
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// onSelected: (suggestion) {
|
||||
// setState(() {
|
||||
// final type = suggestion['type'] as String;
|
||||
// final product = suggestion['product'] as Product;
|
||||
//
|
||||
// selectedProduct = product;
|
||||
//
|
||||
// if (type == 'variant') {
|
||||
// selectedStock = suggestion['stock'] as Stock;
|
||||
// } else {
|
||||
// selectedStock = product.stocks != null && product.stocks!.isNotEmpty ? product.stocks!.first : null;
|
||||
// }
|
||||
//
|
||||
// _searchController.text = type == 'variant' ? "${product.productName} - ${selectedStock?.variantName}" : product.productName ?? '';
|
||||
//
|
||||
// unitController.text = product.unit?.unitName ?? 'Pcs';
|
||||
// priceController.text = (selectedStock?.productPurchasePrice ?? 0).toString();
|
||||
// _calculateTotal();
|
||||
// });
|
||||
//
|
||||
// FocusScope.of(context).unfocus();
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// loading: () => const Center(child: LinearProgressIndicator()),
|
||||
// error: (e, stack) => Text('Error: $e'),
|
||||
// ),
|
||||
// // --------------use typehead---------------------
|
||||
// ] else ...[
|
||||
// TextFormField(
|
||||
// controller: searchController,
|
||||
// readOnly: true,
|
||||
// decoration: const InputDecoration(
|
||||
// labelText: 'Product',
|
||||
// border: OutlineInputBorder(),
|
||||
// filled: true,
|
||||
// fillColor: Color(0xFFF5F5F5),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
SizedBox(height: 20),
|
||||
// --- Row 1: Quantity & Units ---
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: qtyController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.quantity,
|
||||
hintText: 'Ex: 1',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) => _calculateTotal(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: unitController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.units,
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Color(0xFFF5F5F5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// --- Row 2: Purchase Price & Total ---
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: priceController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.purchasePrice,
|
||||
hintText: 'Ex: 20',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) => _calculateTotal(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: totalController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.total,
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Color(0xFFF5F5F5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: DAppColors.kWarning),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
_lang.cancel,
|
||||
style: TextStyle(
|
||||
color: DAppColors.kWarning,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
// minimumSize: Size.fromHeight(48),
|
||||
backgroundColor: const Color(0xFFB71C1C), // Red color
|
||||
),
|
||||
onPressed: () {
|
||||
if (selectedProduct != null && selectedStock != null) {
|
||||
final newItem = ComboItem(
|
||||
product: selectedProduct!,
|
||||
stockData: selectedStock!,
|
||||
quantity: int.tryParse(qtyController.text) ?? 1,
|
||||
manualPurchasePrice: double.tryParse(priceController.text),
|
||||
);
|
||||
widget.onSubmit(newItem);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text("Please select a product")));
|
||||
}
|
||||
},
|
||||
child: Text(_lang.save, style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1225
lib/Screens/Products/add product/add_product.dart
Normal file
1225
lib/Screens/Products/add product/add_product.dart
Normal file
File diff suppressed because it is too large
Load Diff
318
lib/Screens/Products/add product/combo_product_form.dart
Normal file
318
lib/Screens/Products/add product/combo_product_form.dart
Normal file
@@ -0,0 +1,318 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_model.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/invoice_constant.dart' hide kMainColor;
|
||||
import '../../../Provider/product_provider.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import 'add_edit_comboItem.dart';
|
||||
import 'modle/create_product_model.dart';
|
||||
|
||||
// Updated Helper Model to support manual price override
|
||||
class ComboItem {
|
||||
final Product product;
|
||||
final Stock stockData;
|
||||
int quantity;
|
||||
double? manualPurchasePrice; // Added this field
|
||||
|
||||
ComboItem({
|
||||
required this.product,
|
||||
required this.stockData,
|
||||
this.quantity = 1,
|
||||
this.manualPurchasePrice,
|
||||
});
|
||||
|
||||
// Use manual price if set, otherwise stock price
|
||||
double get purchasePrice => manualPurchasePrice ?? (stockData.productPurchasePrice ?? 0).toDouble();
|
||||
double get totalAmount => purchasePrice * quantity;
|
||||
}
|
||||
|
||||
class ComboProductForm extends ConsumerStatefulWidget {
|
||||
final TextEditingController profitController;
|
||||
final TextEditingController saleController;
|
||||
final TextEditingController purchasePriceController;
|
||||
final List<ComboProductModel>? initialComboList;
|
||||
final Function(List<ComboProductModel>) onComboListChanged;
|
||||
|
||||
const ComboProductForm({
|
||||
super.key,
|
||||
required this.profitController,
|
||||
required this.saleController,
|
||||
required this.purchasePriceController,
|
||||
this.initialComboList,
|
||||
required this.onComboListChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ComboProductForm> createState() => _ComboProductFormState();
|
||||
}
|
||||
|
||||
class _ComboProductFormState extends ConsumerState<ComboProductForm> {
|
||||
List<ComboItem> selectedComboItems = [];
|
||||
bool _isDataLoaded = false;
|
||||
|
||||
// --- Calculation Logic (Same as before) ---
|
||||
void _calculateValues({String? source}) {
|
||||
double totalPurchase = 0;
|
||||
for (var item in selectedComboItems) {
|
||||
totalPurchase += item.totalAmount;
|
||||
}
|
||||
|
||||
if (widget.purchasePriceController.text != totalPurchase.toStringAsFixed(2)) {
|
||||
widget.purchasePriceController.text = totalPurchase.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
double purchase = totalPurchase;
|
||||
double profit = double.tryParse(widget.profitController.text) ?? 0;
|
||||
double sale = double.tryParse(widget.saleController.text) ?? 0;
|
||||
|
||||
if (source == 'margin') {
|
||||
sale = purchase + (purchase * profit / 100);
|
||||
widget.saleController.text = sale.toStringAsFixed(2);
|
||||
} else if (source == 'sale') {
|
||||
if (purchase > 0) {
|
||||
profit = ((sale - purchase) / purchase) * 100;
|
||||
widget.profitController.text = profit.toStringAsFixed(2);
|
||||
}
|
||||
} else {
|
||||
sale = purchase + (purchase * profit / 100);
|
||||
widget.saleController.text = sale.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
List<ComboProductModel> finalApiList = selectedComboItems.map((item) {
|
||||
return ComboProductModel(
|
||||
stockId: item.stockData.id.toString(),
|
||||
quantity: item.quantity.toString(),
|
||||
purchasePrice: item.purchasePrice.toString(),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
widget.onComboListChanged(finalApiList);
|
||||
}
|
||||
|
||||
// --- Open the Popup for Add or Edit ---
|
||||
void openProductForm({ComboItem? item, int? index}) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddOrEditComboItem(
|
||||
existingItem: item,
|
||||
onSubmit: (newItem) {
|
||||
setState(() {
|
||||
if (index != null) {
|
||||
// Edit Mode: Replace item
|
||||
selectedComboItems[index] = newItem;
|
||||
} else {
|
||||
// Add Mode: Check duplicate or add new
|
||||
bool exists = false;
|
||||
for (int i = 0; i < selectedComboItems.length; i++) {
|
||||
if (selectedComboItems[i].stockData.id == newItem.stockData.id) {
|
||||
// If same product exists, just update that entry
|
||||
selectedComboItems[i] = newItem;
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exists) selectedComboItems.add(newItem);
|
||||
}
|
||||
_calculateValues(source: 'item_updated');
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final productListAsync = ref.watch(productProvider);
|
||||
final _theme = Theme.of(context);
|
||||
|
||||
// Load Initial Data Logic
|
||||
productListAsync.whenData((products) {
|
||||
if (!_isDataLoaded && widget.initialComboList != null && widget.initialComboList!.isNotEmpty) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
List<ComboItem> tempLoadedItems = [];
|
||||
for (var initialItem in widget.initialComboList!) {
|
||||
for (var product in products) {
|
||||
if (product.stocks != null) {
|
||||
try {
|
||||
var matchingStock =
|
||||
product.stocks!.firstWhere((s) => s.id.toString() == initialItem.stockId.toString());
|
||||
tempLoadedItems.add(ComboItem(
|
||||
product: product,
|
||||
stockData: matchingStock,
|
||||
quantity: int.tryParse(initialItem.quantity.toString()) ?? 1,
|
||||
manualPurchasePrice: double.tryParse(initialItem.purchasePrice.toString()),
|
||||
));
|
||||
break;
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
selectedComboItems = tempLoadedItems;
|
||||
_isDataLoaded = true;
|
||||
});
|
||||
_calculateValues(source: 'init');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. Add Product Button
|
||||
ElevatedButton(
|
||||
onPressed: () => openProductForm(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kMainColor50, // Light reddish background
|
||||
minimumSize: Size(131, 36),
|
||||
elevation: 0,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
),
|
||||
child: Text(
|
||||
"+ ${l.S.of(context).addProduct}",
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: kMainColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 2. List of Items (Matching Screenshot 1)
|
||||
if (selectedComboItems.isNotEmpty)
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: selectedComboItems.length,
|
||||
separatorBuilder: (_, __) => const Divider(
|
||||
height: 1,
|
||||
color: kLineColor,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final item = selectedComboItems[index];
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: 0),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
item.product.productType == 'single'
|
||||
? item.product.productName ?? 'n/a'
|
||||
: ('${item.product.productName ?? ''} (${item.product.stocks?[index].variantName ?? 'n/a'})'),
|
||||
style: _theme.textTheme.bodyLarge,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${l.S.of(context).qty}: ${item.quantity}',
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${l.S.of(context).code} : ${item.product.productCode ?? 'n/a'}, ${l.S.of(context).batchNo}: ${item.stockData.batchNo ?? 'n/a'}',
|
||||
style: _theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$currency${item.totalAmount ?? 'n/a'}',
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 30,
|
||||
child: PopupMenuButton<String>(
|
||||
iconColor: kPeraColor,
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
openProductForm(item: item, index: index);
|
||||
} else if (value == 'delete') {
|
||||
setState(() {
|
||||
selectedComboItems.removeAt(index);
|
||||
_calculateValues(source: 'item_removed');
|
||||
});
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(value: 'edit', child: Text(l.S.of(context).edit)),
|
||||
PopupMenuItem(
|
||||
value: 'delete', child: Text(l.S.of(context).delete, style: TextStyle(color: Colors.red))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
if (selectedComboItems.isNotEmpty)
|
||||
const Divider(
|
||||
height: 1,
|
||||
color: kLineColor,
|
||||
),
|
||||
SizedBox(height: 13),
|
||||
// 3. Footer: Net Total, Profit, Sale Price
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("${l.S.of(context).netTotalAmount}:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
Text("\$${widget.purchasePriceController.text}",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget.profitController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: '${l.S.of(context).profitMargin} (%)',
|
||||
hintText: 'Ex: 25%',
|
||||
border: OutlineInputBorder(),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
),
|
||||
onChanged: (value) => _calculateValues(source: 'margin'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget.saleController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).defaultSellingPrice,
|
||||
hintText: 'Ex: 150',
|
||||
border: OutlineInputBorder(),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
),
|
||||
onChanged: (value) => _calculateValues(source: 'sale'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
150
lib/Screens/Products/add product/modle/create_product_model.dart
Normal file
150
lib/Screens/Products/add product/modle/create_product_model.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'dart:io';
|
||||
|
||||
// Enum for clearer logic in UI
|
||||
enum ProductType { single, variant, combo }
|
||||
|
||||
// --- 1. Combo Product Model ---
|
||||
class ComboProductModel {
|
||||
ComboProductModel({
|
||||
this.stockId,
|
||||
this.quantity,
|
||||
this.purchasePrice,
|
||||
});
|
||||
|
||||
String? stockId;
|
||||
String? quantity;
|
||||
String? purchasePrice;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {
|
||||
'stock_id': stockId,
|
||||
'quantity': quantity,
|
||||
'purchase_price': purchasePrice,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. Stock Data Model (Existing) ---
|
||||
class StockDataModel {
|
||||
StockDataModel({
|
||||
this.stockId,
|
||||
this.batchNo,
|
||||
this.warehouseId,
|
||||
this.productStock,
|
||||
this.exclusivePrice,
|
||||
this.inclusivePrice,
|
||||
this.profitPercent,
|
||||
this.productSalePrice,
|
||||
this.productWholeSalePrice,
|
||||
this.productDealerPrice,
|
||||
this.mfgDate,
|
||||
this.expireDate,
|
||||
this.serialNumbers,
|
||||
this.variantName,
|
||||
this.variationData,
|
||||
this.subStock,
|
||||
});
|
||||
|
||||
String? stockId;
|
||||
String? batchNo;
|
||||
String? warehouseId;
|
||||
String? productStock;
|
||||
String? exclusivePrice;
|
||||
String? inclusivePrice;
|
||||
String? profitPercent;
|
||||
String? productSalePrice;
|
||||
String? productWholeSalePrice;
|
||||
String? productDealerPrice;
|
||||
String? mfgDate;
|
||||
String? expireDate;
|
||||
List<String>? serialNumbers;
|
||||
bool? subStock;
|
||||
String? variantName;
|
||||
List<Map<String, dynamic>>? variationData;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {
|
||||
'stock_id': stockId,
|
||||
'batch_no': batchNo,
|
||||
'warehouse_id': warehouseId,
|
||||
'productStock': productStock,
|
||||
'exclusive_price': exclusivePrice,
|
||||
'inclusive_price': inclusivePrice,
|
||||
'profit_percent': profitPercent == 'Infinity' ? '0' : profitPercent,
|
||||
'productSalePrice': productSalePrice,
|
||||
'productWholeSalePrice': productWholeSalePrice,
|
||||
'productDealerPrice': productDealerPrice,
|
||||
'mfg_date': mfgDate,
|
||||
'expire_date': expireDate,
|
||||
'serial_numbers': serialNumbers,
|
||||
'variant_name': variantName,
|
||||
'variation_data': variationData,
|
||||
};
|
||||
data.removeWhere((key, value) => value == null || value.toString().isEmpty || value == 'null');
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. Main Create Product Model ---
|
||||
class CreateProductModel {
|
||||
CreateProductModel({
|
||||
this.productId,
|
||||
this.name,
|
||||
this.categoryId,
|
||||
this.brandId,
|
||||
this.productCode,
|
||||
this.modelId,
|
||||
this.rackId,
|
||||
this.shelfId,
|
||||
this.alertQty,
|
||||
this.unitId,
|
||||
this.vatId,
|
||||
this.vatType,
|
||||
this.vatAmount,
|
||||
this.image,
|
||||
this.productType,
|
||||
this.stocks,
|
||||
this.comboProducts,
|
||||
this.variationIds,
|
||||
this.warrantyDuration,
|
||||
this.warrantyPeriod,
|
||||
this.guaranteeDuration,
|
||||
this.guaranteePeriod,
|
||||
this.productManufacturer,
|
||||
this.productDiscount,
|
||||
this.comboProfitPercent,
|
||||
this.comboProductSalePrice,
|
||||
});
|
||||
|
||||
String? productId;
|
||||
String? name;
|
||||
String? categoryId;
|
||||
String? brandId;
|
||||
String? productCode;
|
||||
String? modelId;
|
||||
String? rackId;
|
||||
String? shelfId;
|
||||
String? alertQty;
|
||||
String? unitId;
|
||||
String? vatId;
|
||||
String? vatType;
|
||||
String? vatAmount;
|
||||
File? image;
|
||||
String? productType;
|
||||
String? comboProfitPercent;
|
||||
String? comboProductSalePrice;
|
||||
|
||||
// Lists
|
||||
List<StockDataModel>? stocks;
|
||||
List<ComboProductModel>? comboProducts;
|
||||
List<String?>? variationIds;
|
||||
|
||||
String? productManufacturer;
|
||||
String? productDiscount;
|
||||
|
||||
String? warrantyDuration;
|
||||
String? warrantyPeriod;
|
||||
String? guaranteeDuration;
|
||||
String? guaranteePeriod;
|
||||
}
|
||||
373
lib/Screens/Products/add product/single_product_form.dart
Normal file
373
lib/Screens/Products/add product/single_product_form.dart
Normal file
@@ -0,0 +1,373 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/Products/product_setting/model/get_product_setting_model.dart';
|
||||
import 'package:mobile_pos/Screens/warehouse/warehouse_model/warehouse_list_model.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../../constant.dart';
|
||||
import '../../../service/check_user_role_permission_provider.dart';
|
||||
import '../../warehouse/warehouse_provider/warehouse_provider.dart';
|
||||
|
||||
class SingleProductForm extends ConsumerWidget {
|
||||
const SingleProductForm({
|
||||
super.key,
|
||||
required this.snapShot,
|
||||
required this.batchController,
|
||||
required this.stockController,
|
||||
required this.purchaseExController,
|
||||
required this.purchaseIncController,
|
||||
required this.profitController,
|
||||
required this.saleController,
|
||||
required this.wholesaleController,
|
||||
required this.dealerController,
|
||||
required this.mfgDateController,
|
||||
required this.expDateController,
|
||||
this.selectedWarehouse,
|
||||
required this.onWarehouseChanged,
|
||||
required this.onPriceChanged,
|
||||
required this.onMfgDateSelected,
|
||||
required this.onExpDateSelected,
|
||||
});
|
||||
|
||||
final GetProductSettingModel snapShot;
|
||||
|
||||
// Controllers passed from Parent
|
||||
final TextEditingController batchController;
|
||||
final TextEditingController stockController;
|
||||
final TextEditingController purchaseExController;
|
||||
final TextEditingController purchaseIncController;
|
||||
final TextEditingController profitController;
|
||||
final TextEditingController saleController;
|
||||
final TextEditingController wholesaleController;
|
||||
final TextEditingController dealerController;
|
||||
final TextEditingController mfgDateController;
|
||||
final TextEditingController expDateController;
|
||||
|
||||
// State variables passed from Parent
|
||||
final WarehouseData? selectedWarehouse;
|
||||
|
||||
// Callbacks to update Parent State
|
||||
final Function(WarehouseData?) onWarehouseChanged;
|
||||
final Function(String from) onPriceChanged; // To trigger calculation
|
||||
final Function(String date) onMfgDateSelected;
|
||||
final Function(String date) onExpDateSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final permissionService = PermissionService(ref);
|
||||
final warehouseData = ref.watch(fetchWarehouseListProvider);
|
||||
final modules = snapShot.data?.modules;
|
||||
final _lang = lang.S.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
///-------------Batch No & Warehouse----------------------------------
|
||||
if (modules?.showBatchNo == '1' || modules?.showWarehouse == '1') ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showBatchNo == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: batchController,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.batchNo,
|
||||
hintText: _lang.enterBatchNo,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showBatchNo == '1' && modules?.showWarehouse == '1') const SizedBox(width: 14),
|
||||
if (modules?.showWarehouse == '1')
|
||||
Expanded(
|
||||
child: warehouseData.when(
|
||||
data: (dataList) {
|
||||
return Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
children: [
|
||||
DropdownButtonFormField<WarehouseData>(
|
||||
hint: Text(_lang.selectWarehouse),
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.warehouse,
|
||||
),
|
||||
value: selectedWarehouse,
|
||||
icon: selectedWarehouse != null
|
||||
? IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.clear,
|
||||
color: Colors.red,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
onWarehouseChanged.call(null);
|
||||
},
|
||||
)
|
||||
: const Icon(Icons.keyboard_arrow_down_outlined),
|
||||
items: dataList.data
|
||||
?.map(
|
||||
(rack) => DropdownMenuItem<WarehouseData>(
|
||||
value: rack,
|
||||
child: Text(
|
||||
rack.name ?? '',
|
||||
style: const TextStyle(fontWeight: FontWeight.normal),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: onWarehouseChanged,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (e, st) => const Text('Warehouse Load Error'),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
// child: warehouseData.when(
|
||||
// data: (dataList) {
|
||||
// return DropdownButtonFormField<WarehouseData>(
|
||||
// hint: const Text('Select Warehouse'),
|
||||
// isExpanded: true,
|
||||
// decoration: const InputDecoration(labelText: 'Warehouse', border: OutlineInputBorder()),
|
||||
// value: selectedWarehouse,
|
||||
// icon: const Icon(Icons.keyboard_arrow_down_outlined),
|
||||
// items: dataList.data
|
||||
// ?.map(
|
||||
// (rack) => DropdownMenuItem<WarehouseData>(
|
||||
// value: rack,
|
||||
// child: Text(rack.name ?? '', style: const TextStyle(fontWeight: FontWeight.normal)),
|
||||
// ),
|
||||
// )
|
||||
// .toList(),
|
||||
// onChanged: onWarehouseChanged,
|
||||
// );
|
||||
// },
|
||||
// error: (e, st) => const Text('Rack Load Error'),
|
||||
// loading: () => const Center(child: CircularProgressIndicator()),
|
||||
// ),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
if (modules?.showProductStock == '1') const SizedBox(height: 24),
|
||||
|
||||
///-------------Stock--------------------------------------
|
||||
if (modules?.showProductStock == '1')
|
||||
TextFormField(
|
||||
controller: stockController,
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).stock,
|
||||
hintText: lang.S.of(context).enterStock,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
|
||||
///_________Purchase Price (Exclusive & Inclusive)____________________
|
||||
if ((modules?.showExclusivePrice == '1' || modules?.showInclusivePrice == '1') &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value)) ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showExclusivePrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: purchaseExController,
|
||||
onChanged: (value) => onPriceChanged('purchase_ex'),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).purchaseEx,
|
||||
hintText: lang.S.of(context).enterPurchasePrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showExclusivePrice == '1' && modules?.showInclusivePrice == '1') const SizedBox(width: 14),
|
||||
if (modules?.showInclusivePrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: purchaseIncController,
|
||||
onChanged: (value) => onPriceChanged('purchase_inc'),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).purchaseIn,
|
||||
hintText: lang.S.of(context).enterSaltingPrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
///_________Profit Margin & MRP_____________________
|
||||
if (modules?.showProfitPercent == '1' || modules?.showProductSalePrice == '1') ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showProfitPercent == '1' &&
|
||||
(permissionService.hasPermission(Permit.productsPriceView.value)))
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: profitController,
|
||||
onChanged: (value) => onPriceChanged('profit_margin'),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).profitMargin,
|
||||
hintText: lang.S.of(context).enterPurchasePrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showProfitPercent == '1' &&
|
||||
modules?.showProductSalePrice == '1' &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value))
|
||||
const SizedBox(width: 14),
|
||||
if (modules?.showProductSalePrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: saleController,
|
||||
onChanged: (value) => onPriceChanged('mrp'),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).mrp,
|
||||
hintText: lang.S.of(context).enterSaltingPrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
///_______Wholesale & Dealer Price_________________
|
||||
if (modules?.showProductWholesalePrice == '1' || modules?.showProductDealerPrice == '1') ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showProductWholesalePrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: wholesaleController,
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).wholeSalePrice,
|
||||
hintText: lang.S.of(context).enterWholesalePrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showProductWholesalePrice == '1' && modules?.showProductDealerPrice == '1')
|
||||
const SizedBox(width: 14),
|
||||
if (modules?.showProductDealerPrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: dealerController,
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).dealerPrice,
|
||||
hintText: lang.S.of(context).enterDealerPrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
///_______Dates_________________
|
||||
if ((modules?.showMfgDate == '1') || (modules?.showExpireDate == '1')) ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showMfgDate == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
readOnly: true,
|
||||
controller: mfgDateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).manuDate,
|
||||
hintText: lang.S.of(context).selectDate,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(2015, 8),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
onMfgDateSelected(picked.toString());
|
||||
}
|
||||
},
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showMfgDate == '1' && modules?.showExpireDate == '1') const SizedBox(width: 14),
|
||||
if (modules?.showExpireDate == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
readOnly: true,
|
||||
controller: expDateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).expDate,
|
||||
hintText: lang.S.of(context).selectDate,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(2015, 8),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
onExpDateSelected(picked.toString());
|
||||
}
|
||||
},
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
898
lib/Screens/Products/add product/variant_product_form.dart
Normal file
898
lib/Screens/Products/add product/variant_product_form.dart
Normal file
@@ -0,0 +1,898 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/Products/product_setting/model/get_product_setting_model.dart';
|
||||
import 'package:mobile_pos/Screens/warehouse/warehouse_model/warehouse_list_model.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../../constant.dart';
|
||||
import '../../../currency.dart';
|
||||
import '../../../service/check_user_role_permission_provider.dart';
|
||||
import '../../product variation/model/product_variation_model.dart';
|
||||
import '../../product variation/provider/product_variation_provider.dart';
|
||||
import '../../vat_&_tax/model/vat_model.dart';
|
||||
import '../../warehouse/warehouse_provider/warehouse_provider.dart';
|
||||
import '../Widgets/acnoo_multiple_select_dropdown.dart';
|
||||
import '../Widgets/dropdown_styles.dart';
|
||||
import 'modle/create_product_model.dart';
|
||||
|
||||
class VariantProductForm extends ConsumerStatefulWidget {
|
||||
const VariantProductForm({
|
||||
super.key,
|
||||
required this.initialStocks,
|
||||
required this.onStocksUpdated,
|
||||
required this.snapShot,
|
||||
this.selectedWarehouse,
|
||||
required this.onSelectVariation,
|
||||
this.tax,
|
||||
required this.taxType,
|
||||
this.productVariationIds,
|
||||
this.productCode,
|
||||
});
|
||||
|
||||
final List<StockDataModel> initialStocks;
|
||||
final Function(List<StockDataModel>) onStocksUpdated;
|
||||
final Function(List<String?>) onSelectVariation;
|
||||
final GetProductSettingModel snapShot;
|
||||
final VatModel? tax;
|
||||
final String taxType;
|
||||
final List<String>? productVariationIds;
|
||||
final String? productCode;
|
||||
// State variables passed from Parent
|
||||
final WarehouseData? selectedWarehouse; // Received from parent
|
||||
|
||||
@override
|
||||
ConsumerState<VariantProductForm> createState() => _VariantProductFormState();
|
||||
}
|
||||
|
||||
class _VariantProductFormState extends ConsumerState<VariantProductForm> {
|
||||
List<int?> selectedVariation = [];
|
||||
List<VariationData> variationList = [];
|
||||
Map<num?, List<String>?> selectedVariationValues = {};
|
||||
List<StockDataModel> localVariantStocks = [];
|
||||
|
||||
bool isDataInitialized = false;
|
||||
|
||||
final kLoader = const Center(child: CircularProgressIndicator(strokeWidth: 2));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
localVariantStocks = widget.initialStocks;
|
||||
}
|
||||
|
||||
void generateVariants({bool? changeState}) {
|
||||
if (selectedVariation.isEmpty) {
|
||||
setState(() => localVariantStocks.clear());
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
return;
|
||||
}
|
||||
// 1. Gather active Variations (No Change)
|
||||
List<VariationData> activeVariations = [];
|
||||
List<List<String>> activeValues = [];
|
||||
|
||||
for (var id in selectedVariation) {
|
||||
if (id != null &&
|
||||
selectedVariationValues.containsKey(id) &&
|
||||
selectedVariationValues[id] != null &&
|
||||
selectedVariationValues[id]!.isNotEmpty) {
|
||||
var vData = variationList.firstWhere((element) => element.id == id, orElse: () => VariationData());
|
||||
if (vData.id != null) {
|
||||
activeVariations.add(vData);
|
||||
activeValues.add(selectedVariationValues[id]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (activeVariations.isEmpty || activeValues.length != activeVariations.length) {
|
||||
setState(() => localVariantStocks = []);
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
return;
|
||||
}
|
||||
;
|
||||
|
||||
// 2. Calculate Cartesian Product (No Change)
|
||||
List<List<String>> cartesian(List<List<String>> lists) {
|
||||
List<List<String>> result = [[]];
|
||||
for (var list in lists) {
|
||||
result = [
|
||||
for (var a in result)
|
||||
for (var b in list) [...a, b]
|
||||
];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<List<String>> combinations = cartesian(activeValues);
|
||||
List<StockDataModel> newStocks = [];
|
||||
|
||||
String baseCode = widget.productCode ?? "";
|
||||
int counter = 1;
|
||||
for (var combo in combinations) {
|
||||
String variantName = combo.join(" - ");
|
||||
List<Map<String, String>> vData = [];
|
||||
for (int i = 0; i < combo.length; i++) {
|
||||
vData.add({activeVariations[i].name ?? '': combo[i]});
|
||||
}
|
||||
|
||||
// Check if this ROOT variant already exists (to preserve edits)
|
||||
var existingIndex = localVariantStocks.indexWhere((element) => element.variantName == variantName);
|
||||
|
||||
if (existingIndex != -1) {
|
||||
StockDataModel parent = localVariantStocks[existingIndex];
|
||||
|
||||
// Updating batch no according to new code structure
|
||||
if (baseCode.isNotEmpty) {
|
||||
parent.batchNo = "$baseCode-$counter";
|
||||
}
|
||||
newStocks.add(parent);
|
||||
} else {
|
||||
// C. New Root Variant
|
||||
String autoBatchNo = baseCode.isNotEmpty ? "$baseCode-$counter" : "";
|
||||
|
||||
newStocks.add(StockDataModel(
|
||||
profitPercent: '0',
|
||||
variantName: variantName,
|
||||
batchNo: autoBatchNo, // NEW LOGIC: 1002-1
|
||||
variationData: vData,
|
||||
productStock: "0",
|
||||
exclusivePrice: "0",
|
||||
inclusivePrice: "0",
|
||||
productSalePrice: "0",
|
||||
));
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
|
||||
setState(() => localVariantStocks = newStocks);
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
}
|
||||
|
||||
// --- Logic to Initialize Data from Edit Mode ---
|
||||
void _initializeEditData(List<VariationData> allVariations) {
|
||||
if (isDataInitialized) return;
|
||||
if (localVariantStocks.isEmpty && (widget.productVariationIds == null || widget.productVariationIds!.isEmpty))
|
||||
return;
|
||||
|
||||
// 1. Set Selected Variation Types (Example: Size, Color IDs)
|
||||
if (widget.productVariationIds != null) {
|
||||
selectedVariation = widget.productVariationIds!.map((e) => int.tryParse(e)).where((e) => e != null).toList();
|
||||
}
|
||||
|
||||
for (final stock in localVariantStocks) {
|
||||
print('Pioewruwr------------------------> ${stock.variationData}');
|
||||
if (stock.variationData != null) {
|
||||
for (Map<String, dynamic> vMap in stock.variationData!) {
|
||||
print('$vMap');
|
||||
// vMap looks like {"Size": "M"}
|
||||
vMap.forEach((keyName, value) {
|
||||
// Find the ID associated with this Name (e.g., "Size" -> ID 1)
|
||||
final variationObj = allVariations.firstWhere(
|
||||
(element) => element.name?.toLowerCase() == keyName.toLowerCase(),
|
||||
orElse: () => VariationData(),
|
||||
);
|
||||
|
||||
if (variationObj.id != null) {
|
||||
num vId = variationObj.id!;
|
||||
|
||||
// Add value to the list if not exists
|
||||
if (!selectedVariationValues.containsKey(vId)) {
|
||||
selectedVariationValues[vId] = [];
|
||||
}
|
||||
|
||||
if (value is String && !selectedVariationValues[vId]!.contains(value)) {
|
||||
selectedVariationValues[vId]!.add(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isDataInitialized = true;
|
||||
Future.microtask(() => setState(() {}));
|
||||
}
|
||||
|
||||
void _addSubVariation(int parentIndex) {
|
||||
final parentStock = localVariantStocks[parentIndex];
|
||||
|
||||
// Ensure parent has a batch number
|
||||
if (parentStock.batchNo == null || parentStock.batchNo!.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Parent must have a Batch No first")));
|
||||
return;
|
||||
}
|
||||
|
||||
// Count existing children to generate ID (e.g., 1001-1, 1001-2)
|
||||
final String parentBatch = parentStock.batchNo!;
|
||||
int childCount = localVariantStocks
|
||||
.where((element) => element.batchNo != null && element.batchNo!.startsWith("$parentBatch-"))
|
||||
.length;
|
||||
|
||||
String newSubBatch = "$parentBatch-${childCount + 1}";
|
||||
|
||||
// Create Child Stock (Copying basic data from parent if needed, or blank)
|
||||
StockDataModel childStock = StockDataModel(
|
||||
variantName: "${parentStock.variantName} (Sub ${childCount + 1})", // Indicating it's a sub
|
||||
batchNo: '',
|
||||
variationData: parentStock.variationData, // Inherit variation traits
|
||||
profitPercent: parentStock.profitPercent ?? '0',
|
||||
productStock: "0",
|
||||
exclusivePrice: parentStock.exclusivePrice ?? "0",
|
||||
inclusivePrice: parentStock.inclusivePrice ?? "0",
|
||||
productSalePrice: parentStock.productSalePrice ?? "0",
|
||||
warehouseId: parentStock.warehouseId,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
// Insert immediately after the parent (and its existing children)
|
||||
// We insert at parentIndex + 1 + childCount to keep them grouped
|
||||
localVariantStocks.insert(parentIndex + 1 + childCount, childStock);
|
||||
});
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
}
|
||||
|
||||
void _removeVariation(int index) {
|
||||
final stockToRemove = localVariantStocks[index];
|
||||
final String? batchNo = stockToRemove.batchNo;
|
||||
|
||||
setState(() {
|
||||
localVariantStocks.removeAt(index);
|
||||
|
||||
// If it was a parent, remove all its children (Sub-variations)
|
||||
if (batchNo != null && !batchNo.contains('-')) {
|
||||
localVariantStocks
|
||||
.removeWhere((element) => element.batchNo != null && element.batchNo!.startsWith("$batchNo-"));
|
||||
}
|
||||
});
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _dropdownStyle = AcnooDropdownStyle(context);
|
||||
final variationData = ref.watch(variationListProvider);
|
||||
final _theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
|
||||
//------- Variation Type Selection --------------------
|
||||
variationData.when(
|
||||
data: (variation) {
|
||||
variationList = variation.data ?? [];
|
||||
|
||||
// -----------------------------------------
|
||||
// HERE IS THE FIX: Initialize Data Once
|
||||
// -----------------------------------------
|
||||
if (!isDataInitialized && variationList.isNotEmpty) {
|
||||
_initializeEditData(variationList);
|
||||
}
|
||||
|
||||
return AcnooMultiSelectDropdown(
|
||||
menuItemStyleData: _dropdownStyle.multiSelectMenuItemStyle,
|
||||
buttonStyleData: _dropdownStyle.buttonStyle,
|
||||
iconStyleData: _dropdownStyle.iconStyle,
|
||||
dropdownStyleData: _dropdownStyle.dropdownStyle,
|
||||
labelText: lang.S.of(context).selectVariations,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.all(8),
|
||||
hintText: lang.S.of(context).selectItems,
|
||||
),
|
||||
values: selectedVariation,
|
||||
items: variationList.map((item) {
|
||||
return MultiSelectDropdownMenuItem(value: item.id, labelText: item.name ?? '');
|
||||
}).toList(),
|
||||
onChanged: (values) {
|
||||
setState(() {
|
||||
selectedVariation = values?.map((e) => e as int?).toList() ?? [];
|
||||
|
||||
selectedVariationValues.removeWhere((key, value) => !selectedVariation.contains(key));
|
||||
});
|
||||
|
||||
widget.onSelectVariation(values?.map((e) => e.toString()).toList() ?? []);
|
||||
if (selectedVariation.isEmpty) {
|
||||
setState(() => localVariantStocks.clear());
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
} else {
|
||||
generateVariants();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
error: (e, stack) => Center(child: Text(e.toString())),
|
||||
loading: () => kLoader,
|
||||
),
|
||||
|
||||
//----------- Variation Values Selection ---------------
|
||||
if (selectedVariation.isNotEmpty) const SizedBox(height: 24),
|
||||
if (selectedVariation.isNotEmpty)
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: variationList.where((item) => selectedVariation.contains(item.id)).length,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2, mainAxisSpacing: 8, crossAxisSpacing: 8, childAspectRatio: 2.8),
|
||||
itemBuilder: (context, index) {
|
||||
final filteredItems = variationList.where((item) => selectedVariation.contains(item.id)).toList();
|
||||
final varItem = filteredItems[index];
|
||||
return AcnooMultiSelectDropdown<String>(
|
||||
key: GlobalKey(debugLabel: varItem.name),
|
||||
labelText: varItem.name ?? '',
|
||||
values: selectedVariationValues[varItem.id] ?? [],
|
||||
items: (varItem.values ?? []).map((value) {
|
||||
return MultiSelectDropdownMenuItem(value: value, labelText: value);
|
||||
}).toList(),
|
||||
onChanged: (values) {
|
||||
selectedVariationValues[varItem.id?.toInt()] = values != null && values.isNotEmpty ? values : null;
|
||||
|
||||
generateVariants(changeState: false);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
if (selectedVariation.isEmpty) const SizedBox(height: 24),
|
||||
|
||||
// ================= GENERATED VARIANT LIST =================
|
||||
if (localVariantStocks.isNotEmpty) ...[
|
||||
// const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"${lang.S.of(context).selectVariations} (${localVariantStocks.length})",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: localVariantStocks.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final stock = localVariantStocks[index];
|
||||
// Check if this is a Sub-Variation (contains '-')
|
||||
bool isSubVariation = stock.batchNo != null && stock.variantName!.contains('Sub');
|
||||
|
||||
return Container(
|
||||
color: isSubVariation ? Colors.grey.shade50 : Colors.transparent, // Light bg for sub items
|
||||
child: ListTile(
|
||||
onTap: () {
|
||||
showVariantEditSheet(
|
||||
context: context,
|
||||
stock: localVariantStocks[index],
|
||||
snapShot: widget.snapShot,
|
||||
tax: widget.tax,
|
||||
taxType: widget.taxType,
|
||||
onSave: (updatedStock) {
|
||||
setState(() {
|
||||
localVariantStocks[index] = updatedStock;
|
||||
});
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
},
|
||||
);
|
||||
},
|
||||
contentPadding: !isSubVariation ? EdgeInsets.zero : EdgeInsetsDirectional.only(start: 30),
|
||||
// (+) Button only for Parent items
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
leading: !isSubVariation
|
||||
? IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
icon: const Icon(Icons.add, color: kTitleColor),
|
||||
tooltip: lang.S.of(context).addSubVariation,
|
||||
onPressed: () => _addSubVariation(index),
|
||||
)
|
||||
: Icon(Icons.subdirectory_arrow_right,
|
||||
color: Colors.grey, size: 18), // Visual indicator for child
|
||||
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
stock.variantName ?? "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: isSubVariation ? FontWeight.normal : FontWeight.w500,
|
||||
fontSize: isSubVariation ? 13 : 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text.rich(TextSpan(
|
||||
text: '${lang.S.of(context).stock}: ',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: stock.productStock ?? 'n/a',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: isSubVariation ? FontWeight.normal : FontWeight.w500,
|
||||
fontSize: isSubVariation ? 13 : 14,
|
||||
color: kPeraColor),
|
||||
)
|
||||
])),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${lang.S.of(context).batchNo}: ${stock.batchNo ?? 'N/A'}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.bodyMedium
|
||||
?.copyWith(fontSize: isSubVariation ? 13 : 14, color: kPeraColor),
|
||||
),
|
||||
),
|
||||
Text.rich(TextSpan(
|
||||
text: '${lang.S.of(context).sale}: ',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '$currency${stock.productSalePrice ?? 'n/a'}',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: isSubVariation ? FontWeight.normal : FontWeight.w500,
|
||||
fontSize: isSubVariation ? 13 : 14,
|
||||
color: kTitleColor,
|
||||
),
|
||||
)
|
||||
])),
|
||||
],
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 30,
|
||||
child: PopupMenuButton<String>(
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
showVariantEditSheet(
|
||||
context: context,
|
||||
stock: localVariantStocks[index],
|
||||
snapShot: widget.snapShot,
|
||||
tax: widget.tax,
|
||||
taxType: widget.taxType,
|
||||
onSave: (updatedStock) {
|
||||
setState(() {
|
||||
localVariantStocks[index] = updatedStock;
|
||||
});
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
},
|
||||
);
|
||||
} else if (value == 'delete') {
|
||||
_removeVariation(index);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedPencilEdit02,
|
||||
color: kGreyTextColor,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
lang.S.of(context).edit,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Show delete only if sub-variation
|
||||
if (isSubVariation)
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedDelete03,
|
||||
color: kGreyTextColor,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
lang.S.of(context).edit,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showVariantEditSheet({
|
||||
required BuildContext context,
|
||||
required StockDataModel stock,
|
||||
required GetProductSettingModel snapShot,
|
||||
VatModel? tax,
|
||||
required String taxType,
|
||||
required Function(StockDataModel updatedStock) onSave,
|
||||
}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
builder: (context) =>
|
||||
VariantEditSheet(stock: stock, snapShot: snapShot, tax: tax, taxType: taxType, onSave: onSave),
|
||||
);
|
||||
}
|
||||
|
||||
class VariantEditSheet extends ConsumerStatefulWidget {
|
||||
const VariantEditSheet(
|
||||
{super.key,
|
||||
required this.stock,
|
||||
required this.snapShot,
|
||||
required this.tax,
|
||||
required this.taxType,
|
||||
required this.onSave});
|
||||
final StockDataModel stock;
|
||||
final GetProductSettingModel snapShot;
|
||||
final VatModel? tax;
|
||||
final String taxType;
|
||||
final Function(StockDataModel) onSave;
|
||||
@override
|
||||
ConsumerState<VariantEditSheet> createState() => _VariantEditSheetState();
|
||||
}
|
||||
|
||||
class _VariantEditSheetState extends ConsumerState<VariantEditSheet> {
|
||||
late TextEditingController productBatchNumberController;
|
||||
late TextEditingController productStockController;
|
||||
late TextEditingController purchaseExclusivePriceController;
|
||||
late TextEditingController purchaseInclusivePriceController;
|
||||
late TextEditingController profitMarginController;
|
||||
late TextEditingController salePriceController;
|
||||
late TextEditingController wholeSalePriceController;
|
||||
late TextEditingController dealerPriceController;
|
||||
late TextEditingController expireDateController;
|
||||
late TextEditingController manufactureDateController;
|
||||
|
||||
String? selectedExpireDate;
|
||||
String? selectedManufactureDate;
|
||||
String? selectedWarehouseId; // Added variable for Warehouse
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
productBatchNumberController = TextEditingController(text: widget.stock.batchNo ?? '');
|
||||
productStockController = TextEditingController(text: widget.stock.productStock ?? '');
|
||||
purchaseExclusivePriceController = TextEditingController(text: widget.stock.exclusivePrice ?? '');
|
||||
purchaseInclusivePriceController = TextEditingController(text: widget.stock.inclusivePrice ?? '');
|
||||
profitMarginController = TextEditingController(text: widget.stock.profitPercent ?? '');
|
||||
salePriceController = TextEditingController(text: widget.stock.productSalePrice ?? '');
|
||||
wholeSalePriceController = TextEditingController(text: widget.stock.productWholeSalePrice ?? '');
|
||||
dealerPriceController = TextEditingController(text: widget.stock.productDealerPrice ?? '');
|
||||
selectedExpireDate = widget.stock.expireDate;
|
||||
selectedManufactureDate = widget.stock.mfgDate;
|
||||
|
||||
// Initialize Warehouse ID
|
||||
selectedWarehouseId = widget.stock.warehouseId;
|
||||
|
||||
expireDateController = TextEditingController(
|
||||
text: selectedExpireDate != null && selectedExpireDate!.isNotEmpty
|
||||
? DateFormat.yMd().format(DateTime.parse(selectedExpireDate!))
|
||||
: '');
|
||||
manufactureDateController = TextEditingController(
|
||||
text: selectedManufactureDate != null && selectedManufactureDate!.isNotEmpty
|
||||
? DateFormat.yMd().format(DateTime.parse(selectedManufactureDate!))
|
||||
: '');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
productBatchNumberController.dispose();
|
||||
productStockController.dispose();
|
||||
purchaseExclusivePriceController.dispose();
|
||||
purchaseInclusivePriceController.dispose();
|
||||
profitMarginController.dispose();
|
||||
salePriceController.dispose();
|
||||
wholeSalePriceController.dispose();
|
||||
dealerPriceController.dispose();
|
||||
expireDateController.dispose();
|
||||
manufactureDateController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void calculatePurchaseAndMrp({String? from}) {
|
||||
num taxRate = widget.tax?.rate ?? 0;
|
||||
num purchaseExc = num.tryParse(purchaseExclusivePriceController.text) ?? 0;
|
||||
num purchaseInc = num.tryParse(purchaseInclusivePriceController.text) ?? 0;
|
||||
num profitMargin = num.tryParse(profitMarginController.text) ?? 0;
|
||||
num salePrice = num.tryParse(salePriceController.text) ?? 0;
|
||||
|
||||
if (from == 'purchase_inc') {
|
||||
purchaseExc = (taxRate != 0) ? purchaseInc / (1 + taxRate / 100) : purchaseInc;
|
||||
purchaseExclusivePriceController.text = purchaseExc.toStringAsFixed(2);
|
||||
} else {
|
||||
purchaseInc = purchaseExc + (purchaseExc * taxRate / 100);
|
||||
purchaseInclusivePriceController.text = purchaseInc.toStringAsFixed(2);
|
||||
}
|
||||
purchaseExc = num.tryParse(purchaseExclusivePriceController.text) ?? 0;
|
||||
purchaseInc = num.tryParse(purchaseInclusivePriceController.text) ?? 0;
|
||||
num basePrice = widget.taxType.toLowerCase() == 'exclusive' ? purchaseExc : purchaseInc;
|
||||
|
||||
if (from == 'mrp') {
|
||||
salePrice = num.tryParse(salePriceController.text) ?? 0;
|
||||
if (basePrice > 0) {
|
||||
profitMargin = ((salePrice - basePrice) / basePrice) * 100;
|
||||
profitMarginController.text = profitMargin.toStringAsFixed(2);
|
||||
}
|
||||
} else {
|
||||
if (basePrice > 0) {
|
||||
salePrice = basePrice + (basePrice * profitMargin / 100);
|
||||
salePriceController.text = salePrice.toStringAsFixed(2);
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final permissionService = PermissionService(ref);
|
||||
final theme = Theme.of(context);
|
||||
final modules = widget.snapShot.data?.modules;
|
||||
|
||||
// 1. Fetch Warehouse List from Provider
|
||||
final warehouseData = ref.watch(fetchWarehouseListProvider);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Container(
|
||||
decoration:
|
||||
const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Flexible(
|
||||
child: Text('${lang.S.of(context).edit} ${widget.stock.variantName}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, fontSize: 18)),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.close, size: 20, color: Colors.grey))
|
||||
])),
|
||||
const Divider(height: 1, color: kBorderColor),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(children: [
|
||||
// 2. Display Warehouse Dropdown
|
||||
warehouseData.when(
|
||||
data: (data) => DropdownButtonFormField<String>(
|
||||
value: selectedWarehouseId,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).warehouse,
|
||||
hintText: lang.S.of(context).selectWarehouse,
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12)),
|
||||
items: data.data
|
||||
?.map((WarehouseData w) =>
|
||||
DropdownMenuItem<String>(value: w.id.toString(), child: Text(w.name ?? '')))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => selectedWarehouseId = v)),
|
||||
error: (e, s) => const Text('Failed to load warehouse'),
|
||||
loading: () => const Center(child: LinearProgressIndicator())),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (modules?.showBatchNo == '1' || modules?.showProductStock == '1') ...[
|
||||
Row(children: [
|
||||
if (modules?.showBatchNo == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: productBatchNumberController,
|
||||
label: lang.S.of(context).batchNo,
|
||||
hint: "Ex: B-001")),
|
||||
if (modules?.showBatchNo == '1' && modules?.showProductStock == '1') const SizedBox(width: 12),
|
||||
if (modules?.showProductStock == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: productStockController,
|
||||
label: lang.S.of(context).stock,
|
||||
isNumber: true,
|
||||
hint: "Ex: 50"))
|
||||
]),
|
||||
const SizedBox(height: 16)
|
||||
],
|
||||
if ((modules?.showExclusivePrice == '1' || modules?.showInclusivePrice == '1') &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value)) ...[
|
||||
Row(children: [
|
||||
if (modules?.showExclusivePrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: purchaseExclusivePriceController,
|
||||
label: lang.S.of(context).purchaseEx,
|
||||
isNumber: true,
|
||||
hint: "Ex: 100.00",
|
||||
onChanged: (v) => calculatePurchaseAndMrp())),
|
||||
if (modules?.showExclusivePrice == '1' && modules?.showInclusivePrice == '1')
|
||||
const SizedBox(width: 12),
|
||||
if (modules?.showInclusivePrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: purchaseInclusivePriceController,
|
||||
label: lang.S.of(context).purchaseIn,
|
||||
isNumber: true,
|
||||
hint: "Ex: 115.00",
|
||||
onChanged: (v) => calculatePurchaseAndMrp(from: "purchase_inc")))
|
||||
]),
|
||||
const SizedBox(height: 16)
|
||||
],
|
||||
if (modules?.showProfitPercent == '1' || modules?.showProductSalePrice == '1') ...[
|
||||
Row(children: [
|
||||
if (modules?.showProfitPercent == '1' &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value))
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: profitMarginController,
|
||||
label: lang.S.of(context).profitMargin,
|
||||
isNumber: true,
|
||||
hint: "Ex: 20%",
|
||||
onChanged: (v) => calculatePurchaseAndMrp())),
|
||||
if (modules?.showProfitPercent == '1' &&
|
||||
modules?.showProductSalePrice == '1' &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value))
|
||||
const SizedBox(width: 12),
|
||||
if (modules?.showProductSalePrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: salePriceController,
|
||||
label: lang.S.of(context).mrp,
|
||||
isNumber: true,
|
||||
hint: "Ex: 150.00",
|
||||
onChanged: (v) => calculatePurchaseAndMrp(from: 'mrp')))
|
||||
]),
|
||||
const SizedBox(height: 16)
|
||||
],
|
||||
if (modules?.showProductWholesalePrice == '1' || modules?.showProductDealerPrice == '1') ...[
|
||||
Row(children: [
|
||||
if (modules?.showProductWholesalePrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: wholeSalePriceController,
|
||||
label: lang.S.of(context).wholeSalePrice,
|
||||
isNumber: true,
|
||||
hint: "Ex: 130.00")),
|
||||
if (modules?.showProductWholesalePrice == '1' && modules?.showProductDealerPrice == '1')
|
||||
const SizedBox(width: 12),
|
||||
if (modules?.showProductDealerPrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: dealerPriceController,
|
||||
label: lang.S.of(context).dealerPrice,
|
||||
isNumber: true,
|
||||
hint: "Ex: 120.00"))
|
||||
]),
|
||||
const SizedBox(height: 16)
|
||||
],
|
||||
if (modules?.showMfgDate == '1' || modules?.showExpireDate == '1') ...[
|
||||
Row(children: [
|
||||
if (modules?.showMfgDate == '1')
|
||||
Expanded(
|
||||
child: _buildDateField(
|
||||
controller: manufactureDateController,
|
||||
label: lang.S.of(context).manufactureDate,
|
||||
isExpire: false,
|
||||
hint: lang.S.of(context).selectDate)),
|
||||
if (modules?.showMfgDate == '1' && modules?.showExpireDate == '1') const SizedBox(width: 12),
|
||||
if (modules?.showExpireDate == '1')
|
||||
Expanded(
|
||||
child: _buildDateField(
|
||||
controller: expireDateController,
|
||||
label: lang.S.of(context).expDate,
|
||||
isExpire: true,
|
||||
hint: lang.S.of(context).selectDate,
|
||||
))
|
||||
]),
|
||||
const SizedBox(height: 24)
|
||||
],
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// 3. Set the selected warehouse ID to the stock object
|
||||
widget.stock.warehouseId = selectedWarehouseId;
|
||||
|
||||
widget.stock.batchNo = productBatchNumberController.text;
|
||||
widget.stock.productStock = productStockController.text;
|
||||
widget.stock.exclusivePrice = purchaseExclusivePriceController.text;
|
||||
widget.stock.inclusivePrice = purchaseInclusivePriceController.text;
|
||||
widget.stock.profitPercent = profitMarginController.text;
|
||||
widget.stock.productSalePrice = salePriceController.text;
|
||||
widget.stock.productWholeSalePrice = wholeSalePriceController.text;
|
||||
widget.stock.productDealerPrice = dealerPriceController.text;
|
||||
widget.stock.expireDate = selectedExpireDate;
|
||||
widget.stock.mfgDate = selectedManufactureDate;
|
||||
widget.onSave(widget.stock);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(lang.S.of(context).saveVariant))),
|
||||
const SizedBox(height: 16),
|
||||
]),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildField(
|
||||
{required TextEditingController controller,
|
||||
required String label,
|
||||
String? hint,
|
||||
bool isNumber = false,
|
||||
Function(String)? onChanged}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: isNumber ? TextInputType.number : TextInputType.text,
|
||||
inputFormatters: isNumber ? [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))] : [],
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
border: const OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12)));
|
||||
}
|
||||
|
||||
Widget _buildDateField(
|
||||
{required TextEditingController controller, required String label, String? hint, required bool isExpire}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
border: const OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
suffixIcon: const Icon(Icons.calendar_today, size: 18)),
|
||||
onTap: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context, initialDate: DateTime.now(), firstDate: DateTime(2015, 8), lastDate: DateTime(2101));
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
controller.text = DateFormat.yMd().format(picked);
|
||||
if (isExpire) {
|
||||
selectedExpireDate = picked.toString();
|
||||
} else {
|
||||
selectedManufactureDate = picked.toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user