first commit

This commit is contained in:
2026-02-07 15:57:09 +07:00
commit 157096f164
1153 changed files with 415766 additions and 0 deletions

View 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)),
),
),
],
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View 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),
],
);
}
}

View 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;
}

View 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),
],
);
}
}

View 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();
}
});
}
});
}
}