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,704 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:mobile_pos/Provider/product_provider.dart';
import 'package:mobile_pos/constant.dart';
import '../../../Const/api_config.dart';
import '../../../Provider/profile_provider.dart';
import '../../../Provider/transactions_provider.dart';
import '../../../Repository/constant_functions.dart';
import '../../../http_client/custome_http_client.dart';
import '../../../http_client/customer_http_client_get.dart';
import '../../../model/balance_sheet_model.dart' as bs;
import '../../../model/bill_wise_loss_profit_report_model.dart' as bwlprm;
import '../../../model/cashflow_model.dart' as cf;
import '../../../model/loss_profit_model.dart' as lpmodel;
import '../../../model/product_history_model.dart' as phlm;
import '../../../model/sale_transaction_model.dart';
import '../../../model/subscription_report_model.dart' as srm;
import '../../../model/tax_report_model.dart' as trm;
import '../../Customers/Provider/customer_provider.dart';
class SaleRepo {
Future<List<SalesTransactionModel>> fetchSalesList({
bool? salesReturn,
String? type,
String? fromDate,
String? toDate,
}) async {
final client = CustomHttpClientGet(client: http.Client());
// Manually build query string to preserve order
final List<String> queryList = [];
if (salesReturn != null && salesReturn) {
queryList.add('returned-sales=true');
}
if (type != null && type.isNotEmpty) {
queryList.add('duration=$type');
}
if (type == 'custom_date' && fromDate != null && toDate != null && fromDate.isNotEmpty && toDate.isNotEmpty) {
queryList.add('from_date=$fromDate');
queryList.add('to_date=$toDate');
}
final String queryString = queryList.join('&');
final Uri uri = Uri.parse('${APIConfig.url}/sales${queryString.isNotEmpty ? '?$queryString' : ''}');
print(uri);
final response = await client.get(url: uri);
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body) as Map<String, dynamic>;
final list = parsed['data'] as List<dynamic>;
return list.map((json) => SalesTransactionModel.fromJson(json)).toList();
} else {
throw Exception('Failed to fetch Sales List. Status code: ${response.statusCode}');
}
}
Future<SalesTransactionModel?> getSingleSale(int id) async {
final uri = Uri.parse('${APIConfig.url}/sales/$id');
try {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final response = await clientGet.get(url: uri);
print("Fetch Single Single Status: ${response.statusCode}");
print("Fetch Single Single Body: ${response.body}");
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body);
return SalesTransactionModel.fromJson(parsed['data']);
} else {
throw Exception("Failed to fetch sale details");
}
} catch (e) {
throw Exception("Error fetching sale: $e");
}
}
/// Create Sale
Future<SalesTransactionModel?> createSale({
required WidgetRef ref,
required BuildContext context,
required num? partyId,
required String? customerPhone,
required String purchaseDate,
required num discountAmount,
required num discountPercent,
required num unRoundedTotalAmount,
required num totalAmount,
required num roundingAmount,
required num dueAmount,
required num vatAmount,
required num vatPercent,
required num? vatId,
required num changeAmount,
required bool isPaid,
required String paymentType,
required String roundedOption,
required List<CartSaleProducts> products,
required String discountType,
required num shippingCharge,
String? note,
File? image,
}) async {
// 1. Prepare Fields
final fields = _buildCommonFields(
purchaseDate: purchaseDate,
discountAmount: discountAmount,
discountPercent: discountPercent,
totalAmount: totalAmount,
dueAmount: dueAmount,
vatAmount: vatAmount,
vatPercent: vatPercent,
changeAmount: changeAmount,
isPaid: isPaid,
paymentType: paymentType,
discountType: discountType,
shippingCharge: shippingCharge,
roundedOption: roundedOption,
roundingAmount: roundingAmount,
unRoundedTotalAmount: unRoundedTotalAmount,
products: products,
note: note,
partyId: partyId,
vatId: vatId,
);
if (customerPhone != null) fields['customer_phone'] = customerPhone;
// 2. Submit Request
final response = await _submitRequest(
ref: ref,
context: context,
url: '${APIConfig.url}/sales',
method: 'POST',
fields: fields,
image: image,
);
print('Sales Response Data: ${response?.body}');
// 3. Handle Success
if (response != null && response.statusCode == 200) {
final parsedData = jsonDecode(response.body);
_refreshProviders(ref);
return SalesTransactionModel.fromJson(parsedData['data']);
} else if (response != null) {
_handleError(context, response);
}
return null;
}
/// Update Sale
Future<void> updateSale({
required WidgetRef ref,
required BuildContext context,
required num id,
required num? partyId,
required String purchaseDate,
required num discountAmount,
required num discountPercent,
required num unRoundedTotalAmount,
required num totalAmount,
required num dueAmount,
required num vatAmount,
required num vatPercent,
required num? vatId,
required num changeAmount,
required num roundingAmount,
required bool isPaid,
required String paymentType,
required String roundedOption,
required List<CartSaleProducts> products,
required String discountType,
required num shippingCharge,
String? note,
File? image,
}) async {
// 1. Prepare Fields
final fields = _buildCommonFields(
purchaseDate: purchaseDate,
discountAmount: discountAmount,
discountPercent: discountPercent,
totalAmount: totalAmount,
dueAmount: dueAmount,
vatAmount: vatAmount,
vatPercent: vatPercent,
changeAmount: changeAmount,
isPaid: isPaid,
paymentType: paymentType,
discountType: discountType,
shippingCharge: shippingCharge,
roundedOption: roundedOption,
roundingAmount: roundingAmount,
unRoundedTotalAmount: unRoundedTotalAmount,
products: products,
note: note,
partyId: partyId,
vatId: vatId,
);
// Add Method Override for Update
fields['_method'] = 'put';
// 2. Submit Request
final response = await _submitRequest(
ref: ref,
context: context,
url: '${APIConfig.url}/sales/$id',
method: 'POST', // Multipart uses POST with _method field for PUT behavior usually
fields: fields,
image: image,
);
// 3. Handle Success
if (response != null && response.statusCode == 200) {
EasyLoading.showSuccess('Updated successful!');
_refreshProviders(ref);
Navigator.pop(context);
} else if (response != null) {
_handleError(context, response);
}
}
// ------------------------------------------
// Private Helper Methods (The Simplification)
// ------------------------------------------
Map<String, String> _buildCommonFields({
required String purchaseDate,
required num discountAmount,
required num discountPercent,
required num totalAmount,
required num dueAmount,
required num vatAmount,
required num vatPercent,
required num changeAmount,
required bool isPaid,
required String paymentType,
required String discountType,
required num shippingCharge,
required String roundedOption,
required num roundingAmount,
required num unRoundedTotalAmount,
required List<CartSaleProducts> products,
String? note,
num? partyId,
num? vatId,
}) {
final Map<String, String> fields = {
'saleDate': purchaseDate,
'discountAmount': discountAmount.toString(),
'discount_percent': discountPercent.toString(),
'totalAmount': totalAmount.toString(),
'dueAmount': dueAmount.toString(),
'paidAmount': (totalAmount - dueAmount).toString(),
'change_amount': changeAmount.toString(),
'vat_amount': vatAmount.toString(),
'vat_percent': vatPercent.toString(),
'isPaid': isPaid.toString(),
'payments': paymentType,
'discount_type': discountType,
'shipping_charge': shippingCharge.toString(),
'rounding_option': roundedOption,
'rounding_amount': roundingAmount.toStringAsFixed(2),
'actual_total_amount': unRoundedTotalAmount.toString(),
'note': note ?? '',
'products': jsonEncode(products.map((e) => e.toJson()).toList()),
};
if (partyId != null) fields['party_id'] = partyId.toString();
if (vatId != null) fields['vat_id'] = vatId.toString();
return fields;
}
Future<http.Response?> _submitRequest({
required WidgetRef ref,
required BuildContext context,
required String url,
required String method,
required Map<String, String> fields,
File? image,
}) async {
final uri = Uri.parse(url);
try {
var request = http.MultipartRequest(method, uri);
// Add Headers
request.headers.addAll({
'Accept': 'application/json',
'Authorization': await getAuthToken(),
'Content-Type': 'multipart/form-data',
});
// Add Fields
request.fields.addAll(fields);
// Add Image
if (image != null) {
request.files.add(await http.MultipartFile.fromPath('image', image.path));
}
CustomHttpClient customHttpClient = CustomHttpClient(
client: http.Client(),
ref: ref,
context: context,
);
var streamedResponse = await customHttpClient.uploadFile(
url: uri,
file: image,
fileFieldName: 'image',
fields: request.fields,
contentType: 'multipart/form-data',
);
print('POST Sales Data ------------------->\n${request.fields}');
return await http.Response.fromStream(streamedResponse);
} catch (error) {
EasyLoading.dismiss();
final errorMessage = error.toString().replaceFirst('Exception: ', '');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(errorMessage), backgroundColor: kMainColor),
);
return null;
}
}
void _refreshProviders(WidgetRef ref) {
ref.refresh(productProvider);
ref.refresh(partiesProvider);
ref.refresh(salesTransactionProvider);
ref.refresh(businessInfoProvider);
ref.refresh(getExpireDateProvider(ref));
ref.refresh(summaryInfoProvider);
}
void _handleError(BuildContext context, http.Response response) {
EasyLoading.dismiss();
try {
final parsedData = jsonDecode(response.body);
print('reponse :${parsedData}');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Operation failed: ${parsedData['message'] ?? response.reasonPhrase}')),
);
} catch (_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Operation failed: ${response.statusCode}')),
);
}
}
Future<lpmodel.LossProfitModel> getLossProfit({
String? type,
String? fromDate,
String? toDate,
}) async {
try {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final List<String> queryList = [];
if (type != null && type.isNotEmpty) {
queryList.add('duration=$type');
}
if (type == 'custom_date' && fromDate != null && toDate != null && fromDate.isNotEmpty && toDate.isNotEmpty) {
queryList.add('from_date=$fromDate');
queryList.add('to_date=$toDate');
}
final String queryString = queryList.join('&');
final Uri uri = Uri.parse('${APIConfig.url}/reports/loss-profit${queryString.isNotEmpty ? '?$queryString' : ''}');
final response = await clientGet.get(url: uri);
print('Response Status: ${response.statusCode}');
print('Response Body: ${response.body}');
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body);
if (parsed == null) {
throw Exception("Response is null");
}
if (parsed['data'] == null) {
return lpmodel.LossProfitModel.fromJson(parsed);
}
return lpmodel.LossProfitModel.fromJson(parsed['data']);
} else {
throw Exception("Failed to fetch loss profit: ${response.statusCode} - ${response.body}");
}
} catch (e, stack) {
throw Exception("Error fetching loss profit: $e");
}
}
Future<cf.CashflowModel> getCashflow({
String? type,
String? fromDate,
String? toDate,
}) async {
try {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final List<String> queryList = [];
if (type != null && type.isNotEmpty) {
queryList.add('duration=$type');
}
if (type == 'custom_date' && fromDate != null && toDate != null && fromDate.isNotEmpty && toDate.isNotEmpty) {
queryList.add('from_date=$fromDate');
queryList.add('to_date=$toDate');
}
final String queryString = queryList.join('&');
final Uri uri = Uri.parse('${APIConfig.url}/reports/cashflow${queryString.isNotEmpty ? '?$queryString' : ''}');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body);
return cf.CashflowModel.fromJson(parsed);
} else {
throw Exception("Failed to fetch sale details: ${response.statusCode} - ${response.body}");
}
} catch (e) {
throw Exception("Error fetching sale: $e");
}
}
Future<bs.BalanceSheetModel> getBalanceSheet({
String? type,
String? fromDate,
String? toDate,
}) async {
try {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final List<String> queryList = [];
if (type != null && type.isNotEmpty) {
queryList.add('duration=$type');
}
if (type == 'custom_date' && fromDate != null && toDate != null && fromDate.isNotEmpty && toDate.isNotEmpty) {
queryList.add('from_date=$fromDate');
queryList.add('to_date=$toDate');
}
final String queryString = queryList.join('&');
final Uri uri =
Uri.parse('${APIConfig.url}/reports/balance-sheet${queryString.isNotEmpty ? '?$queryString' : ''}');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body);
return bs.BalanceSheetModel.fromJson(parsed);
} else {
throw Exception("Failed to fetch balance sheet details: ${response.statusCode} - ${response.body}");
}
} catch (e) {
throw Exception("Error fetching balance sheet: $e");
}
}
Future<List<srm.SubscriptionReportModel>> getSubscriptionReport({
String? type,
String? fromDate,
String? toDate,
}) async {
try {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final List<String> queryList = [];
if (type != null && type.isNotEmpty) {
queryList.add('duration=$type');
}
if (type == 'custom_date' && fromDate != null && toDate != null && fromDate.isNotEmpty && toDate.isNotEmpty) {
queryList.add('from_date=$fromDate');
queryList.add('to_date=$toDate');
}
final String queryString = queryList.join('&');
final Uri uri =
Uri.parse('${APIConfig.url}/reports/subscription${queryString.isNotEmpty ? '?$queryString' : ''}');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body);
return [...?parsed?["data"].map<srm.SubscriptionReportModel>((x) => srm.SubscriptionReportModel.fromJson(x))];
} else {
throw Exception("Failed to fetch subscription report details: ${response.statusCode} - ${response.body}");
}
} catch (e) {
throw Exception("Error fetching subscription report: $e");
}
}
Future<trm.TaxReportModel> getTaxReport({
String? type,
String? fromDate,
String? toDate,
}) async {
try {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final List<String> queryList = [];
if (type != null && type.isNotEmpty) {
queryList.add('duration=$type');
}
if (type == 'custom_date' && fromDate != null && toDate != null && fromDate.isNotEmpty && toDate.isNotEmpty) {
queryList.add('from_date=$fromDate');
queryList.add('to_date=$toDate');
}
final String queryString = queryList.join('&');
final Uri uri = Uri.parse('${APIConfig.url}/reports/tax${queryString.isNotEmpty ? '?$queryString' : ''}');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body);
return trm.TaxReportModel.fromJson(parsed);
} else {
throw Exception("Failed to fetch tax report details: ${response.statusCode} - ${response.body}");
}
} catch (e) {
throw Exception("Error fetching tax report: $e");
}
}
Future<bwlprm.BillWiseLossProfitReportModel> getBillWiseLossProfitReport({
String? type,
String? fromDate,
String? toDate,
}) async {
try {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final List<String> queryList = [];
if (type != null && type.isNotEmpty) {
queryList.add('duration=$type');
}
if (type == 'custom_date' && fromDate != null && toDate != null && fromDate.isNotEmpty && toDate.isNotEmpty) {
queryList.add('from_date=$fromDate');
queryList.add('to_date=$toDate');
}
final String queryString = queryList.join('&');
final Uri uri =
Uri.parse('${APIConfig.url}/reports/bill-wise-profit${queryString.isNotEmpty ? '?$queryString' : ''}');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body);
return bwlprm.BillWiseLossProfitReportModel.fromJson(parsed);
} else {
throw Exception("Failed to fetch tax report details: ${response.statusCode} - ${response.body}");
}
} catch (e) {
throw Exception("Error fetching tax report: $e");
}
}
Future<phlm.ProductHistoryListModel> getProductSaleHistoryReport({
String? type,
String? fromDate,
String? toDate,
}) async {
try {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final List<String> queryList = [];
if (type != null && type.isNotEmpty) {
queryList.add('duration=$type');
}
if (type == 'custom_date' && fromDate != null && toDate != null && fromDate.isNotEmpty && toDate.isNotEmpty) {
queryList.add('from_date=$fromDate');
queryList.add('to_date=$toDate');
}
final String queryString = queryList.join('&');
final Uri uri =
Uri.parse('${APIConfig.url}/reports/product-sale-history${queryString.isNotEmpty ? '?$queryString' : ''}');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body);
return phlm.ProductHistoryListModel.fromJson(parsed);
} else {
throw Exception("Failed to fetch tax report details: ${response.statusCode} - ${response.body}");
}
} catch (e) {
throw Exception("Error fetching tax report: $e");
}
}
Future<phlm.ProductHistoryDetailsModel> getProductSaleHistoryReportDetails({
required int productId,
String? type,
String? fromDate,
String? toDate,
}) async {
try {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final List<String> queryList = [];
if (type != null && type.isNotEmpty) {
queryList.add('duration=$type');
}
if (type == 'custom_date' && fromDate != null && toDate != null && fromDate.isNotEmpty && toDate.isNotEmpty) {
queryList.add('from_date=$fromDate');
queryList.add('to_date=$toDate');
}
final String queryString = queryList.join('&');
final Uri uri = Uri.parse(
'${APIConfig.url}/reports/product-sale-history/$productId${queryString.isNotEmpty ? '?$queryString' : ''}');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body);
return phlm.ProductHistoryDetailsModel.fromJson(parsed);
} else {
throw Exception("Failed to fetch tax report details: ${response.statusCode} - ${response.body}");
}
} catch (e) {
throw Exception("Error fetching tax report: $e");
}
}
Future<phlm.ProductHistoryDetailsModel> getProductPurchaseHistoryReportDetails({
required int productId,
String? type,
String? fromDate,
String? toDate,
}) async {
try {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final List<String> queryList = [];
if (type != null && type.isNotEmpty) {
queryList.add('duration=$type');
}
if (type == 'custom_date' && fromDate != null && toDate != null && fromDate.isNotEmpty && toDate.isNotEmpty) {
queryList.add('from_date=$fromDate');
queryList.add('to_date=$toDate');
}
final String queryString = queryList.join('&');
final Uri uri = Uri.parse(
'${APIConfig.url}/reports/product-purchase-history/$productId${queryString.isNotEmpty ? '?$queryString' : ''}');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body);
return phlm.ProductHistoryDetailsModel.fromJson(parsed);
} else {
throw Exception("Failed to fetch tax report details: ${response.statusCode} - ${response.body}");
}
} catch (e) {
throw Exception("Error fetching tax report: $e");
}
}
}
class CartSaleProducts {
final num stockId;
final num productId;
final num? price;
final num? discount;
final String productName;
final num? quantities;
CartSaleProducts({
required this.productName,
required this.stockId,
this.discount,
required this.productId,
required this.price,
required this.quantities,
});
Map<String, dynamic> toJson() => {
'stock_id': stockId,
'product_id': productId,
'product_name': productName,
'price': price,
'quantities': quantities,
'discount': discount,
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,272 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/Sales/provider/sales_cart_provider.dart';
import 'package:mobile_pos/Screens/Products/Model/product_model.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import '../../currency.dart';
import '../../model/add_to_cart_model.dart';
import '../../service/check_user_role_permission_provider.dart';
Future<void> showAddItemPopup({
required BuildContext mainContext,
required Product productModel,
required WidgetRef ref,
required String? customerType,
required bool fromPOSSales,
}) async {
TextEditingController _searchController = TextEditingController();
final product = productModel;
final permissionService = PermissionService(ref);
List<SaleCartModel> tempCartItemList = [];
List<TextEditingController> controllers = [];
if (product.stocks?.isNotEmpty ?? false) {
final cartList = ref.read(cartNotifier).cartItemList;
for (var element in product.stocks!) {
num sentProductPrice;
if (customerType != null) {
if (customerType.contains('Dealer')) {
sentProductPrice = element.productDealerPrice ?? 0;
} else if (customerType.contains('Wholesaler')) {
sentProductPrice = element.productWholeSalePrice ?? 0;
} else if (customerType.contains('Supplier')) {
sentProductPrice = element.productPurchasePrice ?? 0;
} else {
sentProductPrice = element.productSalePrice ?? 0;
}
} else {
sentProductPrice = element.productSalePrice ?? 0;
}
final existingCartItem = cartList.firstWhere(
(cartItem) => cartItem.productId == product.id && cartItem.stockId == element.id,
orElse: () => SaleCartModel(productId: -1, batchName: '', stockId: 0), // default not-found case
);
final existingQuantity = existingCartItem.productId != -1 ? existingCartItem.quantity : 0;
controllers.add(TextEditingController(text: existingQuantity.toString()));
tempCartItemList.add(SaleCartModel(
batchName: element.batchNo ?? 'N/A',
productName: product.productName,
stockId: element.id ?? 0,
unitPrice: sentProductPrice,
productType: product.productType,
productCode: product.productCode,
productPurchasePrice: element.productPurchasePrice,
stock: element.productStock,
productId: product.id ?? 0,
quantity: existingQuantity,
));
}
}
showDialog(
context: mainContext,
barrierDismissible: false,
builder: (BuildContext context) {
return StatefulBuilder(builder: (context, setState) {
void updateQuantity(int change, int index) {
int currentQty = int.tryParse(controllers[index].text) ?? 0;
int updatedQty = currentQty + change;
if (updatedQty > (tempCartItemList[index].stock ?? 0)) return;
if (updatedQty < 0) return;
setState(() {
controllers[index].text = updatedQty.toString();
});
}
_searchController.addListener(
() {
setState(() {});
},
);
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
insetPadding: EdgeInsets.all(20),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
/// Title Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(product.productName ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
IconButton(
icon: Icon(Icons.close),
onPressed: () => Navigator.pop(context),
)
],
),
SizedBox(height: 8),
/// Search Field
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: lang.S.of(context).searchBatchNo,
prefixIcon: Icon(Icons.search),
contentPadding: EdgeInsets.symmetric(vertical: 0, horizontal: 12),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
),
SizedBox(height: 16),
SizedBox(
height: 250,
child: SingleChildScrollView(
child: Column(
children: [
/// Batch List
...productModel.stocks!.map((item) => Visibility(
visible: _searchController.text.isEmpty ||
(item.batchNo?.toLowerCase().contains(_searchController.text.toLowerCase()) ??
true),
child: Column(
children: [
Row(
children: [
/// Batch Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${lang.S.of(context).batch}: ${item.batchNo ?? 'N/A'}',
style: TextStyle(fontWeight: FontWeight.w600)),
Text('${lang.S.of(context).stock}: ${item.productStock}',
style: TextStyle(color: Colors.green)),
],
),
),
/// Price
// if (permissionService.hasPermission(Permit.salesPriceView.value))
Text('$currency${item.productSalePrice}',
style: TextStyle(fontWeight: FontWeight.w600)),
SizedBox(width: 12),
/// Quantity Controller with Round Buttons
Row(
children: [
/// - Button
InkWell(
onTap: () => updateQuantity(-1, productModel.stocks?.indexOf(item) ?? 0),
borderRadius: BorderRadius.circular(20),
child: Container(
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey.shade200,
),
child: Icon(Icons.remove, size: 16),
),
),
SizedBox(width: 8),
/// Quantity TextField
Container(
width: 60,
height: 32,
alignment: Alignment.center,
child: TextFormField(
controller: controllers[productModel.stocks?.indexOf(item) ?? 0],
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
style: TextStyle(fontSize: 14),
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
isDense: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
),
),
onChanged: (val) {
final parsed = int.tryParse(val);
if (parsed == null ||
parsed < 0 ||
parsed > (item.productStock ?? 0)) {
controllers[productModel.stocks?.indexOf(item) ?? 0].text = '';
}
},
),
),
SizedBox(width: 8),
/// + Button
InkWell(
onTap: () => updateQuantity(1, productModel.stocks?.indexOf(item) ?? 0),
borderRadius: BorderRadius.circular(20),
child: Container(
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey.shade200,
),
child: Icon(Icons.add, size: 16),
),
),
],
),
],
),
Divider(),
],
),
)),
],
),
),
),
SizedBox(height: 16),
/// Add to Cart Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: kMainColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: EdgeInsets.symmetric(vertical: 14),
),
onPressed: () {
for (var element in tempCartItemList) {
element.quantity = num.tryParse(controllers[tempCartItemList.indexOf(element)].text) ?? 0;
}
tempCartItemList.removeWhere((element) => element.quantity <= 0);
for (var element in tempCartItemList) {
ref
.read(cartNotifier)
.addToCartRiverPod(cartItem: element, fromEditSales: false, isVariant: true);
}
if (!fromPOSSales) Navigator.pop(context);
Navigator.pop(context);
},
child: Text(lang.S.of(context).addedToCart, style: TextStyle(fontSize: 16, color: Colors.white)),
),
),
],
),
),
),
);
});
},
);
}

View File

@@ -0,0 +1,225 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Provider/profile_provider.dart';
import 'package:mobile_pos/model/business_info_model.dart';
import '../../Settings/sales settings/model/amount_rounding_dropdown_model.dart';
import '../../vat_&_tax/model/vat_model.dart';
import '../../../model/add_to_cart_model.dart';
final cartNotifier = ChangeNotifierProvider((ref) {
return CartNotifier(businessInformation: ref.watch(businessInfoProvider).value);
});
class CartNotifier extends ChangeNotifier {
final BusinessInformationModel? businessInformation;
CartNotifier({required this.businessInformation});
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
roundedOption = businessInformation?.data?.saleRoundingOption ?? roundingMethods[0].value;
}
List<SaleCartModel> cartItemList = [];
TextEditingController discountTextControllerFlat = TextEditingController();
TextEditingController vatAmountController = TextEditingController();
TextEditingController shippingChargeController = TextEditingController();
///_________NEW_________________________________
num totalAmount = 0;
num discountAmount = 0;
num discountPercent = 0;
num roundingAmount = 0;
num actualTotalAmount = 0;
num totalPayableAmount = 0;
VatModel? selectedVat;
num vatAmount = 0;
bool isFullPaid = false;
num receiveAmount = 0;
num changeAmount = 0;
num dueAmount = 0;
num finalShippingCharge = 0;
String roundedOption = roundingMethods[0].value;
void changeSelectedVat({VatModel? data}) {
if (data != null) {
selectedVat = data;
} else {
selectedVat = null;
vatAmount = 0;
vatAmountController.clear();
}
calculatePrice();
}
void calculateDiscount({required String value, bool? rebuilding, String? selectedTaxType}) {
if (value.isEmpty) {
discountAmount = 0;
discountPercent = 0;
discountTextControllerFlat.clear();
} else {
num discountValue = num.tryParse(value) ?? 0;
if (selectedTaxType == null) {
EasyLoading.showError('Please select a discount type');
discountAmount = 0;
discountPercent = 0;
} else if (selectedTaxType == "Flat") {
discountAmount = discountValue;
} else if (selectedTaxType == "Percent") {
discountPercent = num.tryParse(discountTextControllerFlat.text) ?? 0.0;
discountAmount = (totalAmount * discountValue) / 100;
if (discountAmount > totalAmount) {
discountAmount = totalAmount;
}
} else {
EasyLoading.showError('Invalid discount type selected');
discountAmount = 0;
}
if (discountAmount > totalAmount) {
discountTextControllerFlat.clear();
discountAmount = 0;
EasyLoading.showError('Enter a valid discount');
}
}
if (rebuilding == false) return;
calculatePrice();
}
void updateProduct({required num productId, required String price, required String qty, required num discount}) {
int index = cartItemList.indexWhere((element) => element.productId == productId);
if (index != -1) {
cartItemList[index].unitPrice = num.tryParse(price);
cartItemList[index].quantity = num.tryParse(qty) ?? 0;
cartItemList[index].discountAmount = discount; // Store the product-wise discount
calculatePrice();
}
}
void calculatePrice({String? receivedAmount, String? shippingCharge, bool? stopRebuild}) {
totalAmount = 0;
totalPayableAmount = 0;
dueAmount = 0;
// Calculate Subtotal with Product-wise Discounts
for (var element in cartItemList) {
num unitPrice = element.unitPrice ?? 0;
num productDiscount = element.discountAmount ?? 0;
num quantity = element.quantity;
// Formula: (Unit Price - Discount) * Quantity
// Note: The validation in the form ensures Discount <= Unit Price
totalAmount += (unitPrice - productDiscount) * quantity;
}
totalPayableAmount = totalAmount;
// Apply Global Discount (on the already discounted subtotal)
if (discountAmount > totalAmount) {
calculateDiscount(
value: discountAmount.toString(),
rebuilding: false,
);
}
if (discountAmount >= 0) {
totalPayableAmount -= discountAmount;
}
// Apply VAT
if (selectedVat?.rate != null) {
vatAmount = (totalPayableAmount * selectedVat!.rate!) / 100;
vatAmountController.text = vatAmount.toStringAsFixed(2);
}
totalPayableAmount += vatAmount;
// Apply Shipping
if (shippingCharge != null) {
finalShippingCharge = num.tryParse(shippingCharge) ?? 0;
}
totalPayableAmount += finalShippingCharge;
// Rounding
actualTotalAmount = totalPayableAmount;
num tempTotalPayable = roundNumber(value: totalPayableAmount, roundingType: roundedOption);
roundingAmount = tempTotalPayable - totalPayableAmount;
totalPayableAmount = tempTotalPayable;
// Payment Calculation
if (receivedAmount != null) {
receiveAmount = num.tryParse(receivedAmount) ?? 0;
}
changeAmount = totalPayableAmount < receiveAmount ? receiveAmount - totalPayableAmount : 0;
dueAmount = totalPayableAmount < receiveAmount ? 0 : totalPayableAmount - receiveAmount;
if (dueAmount <= 0) isFullPaid = true;
if (stopRebuild ?? false) return;
notifyListeners();
}
void quantityIncrease(int index) {
final item = cartItemList[index];
final isCombo = item.productType?.toLowerCase().contains('combo') ?? false;
final stock = item.stock ?? 0;
final quantity = item.quantity;
// Allow increase if it's a Combo OR if stock is available
if (isCombo || stock > quantity) {
// If not a combo, perform strict stock check
if (!isCombo && stock < quantity + 1) {
cartItemList[index].quantity = stock;
} else {
cartItemList[index].quantity++;
}
calculatePrice();
} else {
EasyLoading.showError('Stock Overflow');
}
}
void quantityDecrease(int index) {
if (cartItemList[index].quantity > 1) {
cartItemList[index].quantity--;
}
calculatePrice();
}
void addToCartRiverPod({
required SaleCartModel cartItem,
bool? fromEditSales,
bool? isVariant,
}) {
final variantMode = isVariant ?? false;
final index = cartItemList.indexWhere((element) => variantMode ? element.stockId == cartItem.stockId : element.productId == cartItem.productId);
if (index != -1) {
variantMode ? cartItemList[index].quantity = cartItem.quantity : cartItemList[index].quantity++;
} else {
cartItemList.add(cartItem);
}
if (!(fromEditSales ?? false)) {
calculatePrice();
}
}
void deleteToCart(int index) {
cartItemList.removeAt(index);
calculatePrice();
}
void deleteAllVariant({required num productId}) {
cartItemList.removeWhere(
(element) => element.productId == productId,
);
calculatePrice();
}
}

View File

@@ -0,0 +1,207 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/Sales/provider/sales_cart_provider.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import 'package:mobile_pos/model/add_to_cart_model.dart';
import '../../constant.dart';
class SalesAddToCartForm extends StatefulWidget {
const SalesAddToCartForm({
super.key,
required this.batchWiseStockModel,
required this.previousContext,
});
final SaleCartModel batchWiseStockModel;
final BuildContext previousContext;
@override
ProductAddToCartFormState createState() => ProductAddToCartFormState();
}
class ProductAddToCartFormState extends State<SalesAddToCartForm> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// Controllers
late TextEditingController productQuantityController;
late TextEditingController discountController;
late TextEditingController salePriceController;
bool isClicked = false;
@override
void initState() {
super.initState();
// Initialize controllers with existing data
salePriceController = TextEditingController(
text: widget.batchWiseStockModel.unitPrice.toString(),
);
productQuantityController = TextEditingController(
text: formatPointNumber(widget.batchWiseStockModel.quantity),
);
discountController = TextEditingController(
text: widget.batchWiseStockModel.discountAmount?.toString() ?? '',
);
}
@override
void dispose() {
productQuantityController.dispose();
discountController.dispose();
salePriceController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Consumer(builder: (context, ref, __) {
final lang = l.S.of(context);
return Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// --- Quantity and Price Row ---
Row(
children: [
// Quantity Field
Expanded(
child: TextFormField(
controller: productQuantityController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}')),
],
validator: (value) {
final qty = num.tryParse(value ?? '') ?? 0;
// 1. Check for basic valid quantity
if (value == null || value.isEmpty || qty <= 0) {
return lang.enterQuantity;
}
// 2. Check Stock (Skip check if it is a Combo product)
final isCombo = widget.batchWiseStockModel.productType?.toLowerCase().contains('combo') ?? false;
final currentStock = widget.batchWiseStockModel.stock ?? 0;
if (!isCombo && qty > currentStock) {
return lang.outOfStock;
}
return null;
},
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
label: Text(lang.quantity),
hintText: lang.enterQuantity,
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 15),
),
),
),
const SizedBox(width: 10),
// Sale Price Field
Expanded(
child: TextFormField(
controller: salePriceController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}')),
],
validator: (value) {
if (value == null || value.isEmpty) {
return lang.pleaseEnterAValidSalePrice;
}
return null;
},
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
label: Text(lang.salePrice),
hintText: lang.enterAmount,
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 15),
),
),
),
],
),
const SizedBox(height: 15),
// --- Discount Field ---
TextFormField(
controller: discountController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}')),
],
validator: (value) {
if (value != null && value.isNotEmpty) {
final discount = num.tryParse(value) ?? 0;
final price = num.tryParse(salePriceController.text) ?? 0;
if (discount < 0) {
return lang.enterAValidDiscount; // Or "Discount cannot be negative"
}
// Validation: Discount cannot be greater than the unit price
if (discount > price) {
return '${lang.discount} > ${lang.salePrice}';
}
}
return null;
},
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
label: Text(lang.discount),
hintText: lang.enterAValidDiscount,
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 15),
),
),
const SizedBox(height: 29),
// --- Save Button ---
GestureDetector(
onTap: () async {
if (isClicked) return;
if (_formKey.currentState?.validate() ?? false) {
setState(() {
isClicked = true;
});
ref.read(cartNotifier).updateProduct(
productId: widget.batchWiseStockModel.productId,
price: salePriceController.text,
qty: productQuantityController.text,
discount: num.tryParse(discountController.text) ?? 0,
);
Navigator.pop(context);
}
},
child: Container(
height: 48,
decoration: const BoxDecoration(
color: kMainColor,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
child: Center(
child: Text(
lang.save,
style: const TextStyle(fontSize: 18, color: Colors.white),
),
),
),
),
Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
)
],
),
);
});
}
}

View File

@@ -0,0 +1,207 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hugeicons/hugeicons.dart';
import 'package:mobile_pos/Screens/Sales/provider/sales_cart_provider.dart';
import 'package:mobile_pos/Screens/Sales/sales_add_to_cart_sales_widget.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
class SalesCartListWidget extends ConsumerWidget {
const SalesCartListWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final providerData = ref.watch(cartNotifier);
final s = lang.S.of(context);
final _theme = Theme.of(context);
return providerData.cartItemList.isNotEmpty
? Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
initiallyExpanded: true,
collapsedBackgroundColor: kMainColor2,
backgroundColor: kMainColor2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: kLineColor,
width: 1,
),
),
title: Text(
lang.S.of(context).itemAdded,
style: _theme.textTheme.titleMedium,
),
children: [
Container(
color: Colors.white,
child: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: providerData.cartItemList.length,
itemBuilder: (context, index) {
final item = providerData.cartItemList[index];
// Calculate values for display
final double quantity = item.quantity.toDouble();
final double unitPrice = (item.unitPrice ?? 0).toDouble();
final double discountPerUnit = (item.discountAmount ?? 0).toDouble();
final double totalDiscount = quantity * discountPerUnit;
final double subTotal = quantity * unitPrice;
final double finalTotal = subTotal - totalDiscount;
return ListTile(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
contentPadding: EdgeInsetsDirectional.symmetric(horizontal: 10),
onTap: () => showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context2) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
s.updateProduct,
style: const TextStyle(fontWeight: FontWeight.bold),
),
CloseButton(
onPressed: () => Navigator.pop(context2),
)
],
),
),
const Divider(thickness: 1, color: kBorderColorTextField),
Padding(
padding: const EdgeInsets.all(16.0),
child: SalesAddToCartForm(
batchWiseStockModel: item,
previousContext: context2,
),
),
],
);
},
),
title: Text(
item.productName.toString(),
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
subtitle: RichText(
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
// Qty X Price
TextSpan(
text: '${formatPointNumber(quantity)} X $unitPrice ',
style: _theme.textTheme.titleSmall?.copyWith(
color: kPeraColor,
),
),
// Show Discount if exists
if (totalDiscount > 0)
TextSpan(
text: '- ${formatPointNumber(totalDiscount)} (Disc) ',
style: _theme.textTheme.titleSmall?.copyWith(
color: kPeraColor,
),
),
// Final Total
TextSpan(
text: '= ${formatPointNumber(finalTotal)} ',
style: _theme.textTheme.titleSmall?.copyWith(
color: kTitleColor,
),
),
// Batch Info
if (item.productType == 'variant')
TextSpan(
text: '[${item.batchName}]',
style: const TextStyle(fontSize: 12, fontStyle: FontStyle.italic),
),
],
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 90,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: () => providerData.quantityDecrease(index),
child: Container(
height: 18,
width: 18,
decoration: const BoxDecoration(
color: kMainColor,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
child: const Center(
child: Icon(Icons.remove, size: 14, color: Colors.white),
),
),
),
const SizedBox(width: 5),
SizedBox(
width: 40,
child: Center(
child: Text(
formatPointNumber(item.quantity),
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: kGreyTextColor,
),
maxLines: 1,
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () => providerData.quantityIncrease(index),
child: Container(
height: 18,
width: 18,
decoration: const BoxDecoration(
color: kMainColor,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
child: const Center(
child: Icon(Icons.add, size: 14, color: Colors.white),
),
),
),
],
),
),
const SizedBox(width: 12),
GestureDetector(
onTap: () => providerData.deleteToCart(index),
child: HugeIcon(
icon: HugeIcons.strokeRoundedDelete03,
size: 20,
color: Colors.red,
),
),
],
),
);
},
),
)
],
),
),
)
: SizedBox.shrink();
}
}

View File

@@ -0,0 +1,326 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Provider/product_provider.dart';
import 'package:mobile_pos/Screens/Customers/Model/parties_model.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:nb_utils/nb_utils.dart';
import '../../Const/api_config.dart';
import '../../GlobalComponents/bar_code_scaner_widget.dart';
import '../../GlobalComponents/glonal_popup.dart';
import 'provider/sales_cart_provider.dart';
import '../../currency.dart';
import '../../model/add_to_cart_model.dart';
import '../Products/add product/add_product.dart';
import '../../service/check_user_role_permission_provider.dart';
import '../Products/add product/modle/create_product_model.dart';
import 'batch_select_popup_sales.dart';
class SaleProductsList extends StatefulWidget {
const SaleProductsList({super.key, this.customerModel});
final Party? customerModel;
@override
// ignore: library_private_types_in_public_api
_SaleProductsListState createState() => _SaleProductsListState();
}
class _SaleProductsListState extends State<SaleProductsList> {
String productCode = '0000';
TextEditingController codeController = TextEditingController();
@override
Widget build(BuildContext context) {
return GlobalPopup(
child: Consumer(builder: (context, ref, __) {
final providerData = ref.watch(cartNotifier);
final productList = ref.watch(productProvider);
return Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
title: Text(
lang.S.of(context).addItems,
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0.0,
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: SingleChildScrollView(
child: Column(
children: [
Row(
children: [
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: AppTextField(
controller: codeController,
textFieldType: TextFieldType.NAME,
onChanged: (value) {
setState(() {
productCode = value.trim();
});
},
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).productCode,
hintText: (productCode == '0000' || productCode == '-1' || productCode.isEmpty)
? lang.S.of(context).scanCode
: productCode,
border: const OutlineInputBorder(),
),
),
),
),
Expanded(
flex: 1,
child: GestureDetector(
onTap: () async {
showDialog(
context: context,
builder: (context) => BarcodeScannerWidget(
onBarcodeFound: (String code) {
setState(() {
productCode = code;
codeController.text = productCode;
});
},
),
);
},
child: const BarCodeButton(),
),
),
],
),
productList.when(
data: (products) {
final filteredProducts = products.where((product) {
final codeMatch = product.productCode == productCode ||
productCode == '0000' ||
productCode == '-1' ||
productCode.isEmpty;
final nameMatch =
(product.productName?.toLowerCase() ?? '').contains(productCode.toLowerCase());
// --- Logic Update Starts Here ---
bool isCombo = product.productType?.toLowerCase().contains('combo') ?? false;
bool hasStock = (product.stocksSumProductStock ?? 0) > 0;
// If it is NOT a combo, it MUST have stock to be shown.
// If it IS a combo, we show it regardless of the specific 'stocksSumProductStock' field
// (unless you specifically want to hide empty combos too).
if (!isCombo && !hasStock) {
return false;
}
// --- Logic Update Ends Here ---
return codeMatch || nameMatch;
}).toList();
if (filteredProducts.isEmpty) {
return Center(
child: Text(lang.S.of(context).noProductFound),
);
}
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredProducts.length,
itemBuilder: (_, i) {
final product = filteredProducts[i];
final isCombo = product.productType?.toLowerCase().contains('combo') ?? false;
num sentProductPrice = 0;
final stock =
(product.stocks != null && product.stocks!.isNotEmpty) ? product.stocks!.first : null;
// Determine display text for stock
String stockDisplayText;
if (isCombo) {
stockDisplayText = "Combo";
sentProductPrice = product.productSalePrice ?? 0;
} else {
stockDisplayText = '${lang.S.of(context).stocks}${product.stocksSumProductStock ?? 0}';
if (widget.customerModel?.type != null) {
final type = widget.customerModel!.type!;
if (type.contains('Dealer')) {
sentProductPrice = stock?.productDealerPrice ?? 0;
} else if (type.contains('Wholesaler')) {
sentProductPrice = stock?.productWholeSalePrice ?? 0;
} else if (type.contains('Supplier')) {
sentProductPrice = stock?.productPurchasePrice ?? 0;
} else {
sentProductPrice = stock?.productSalePrice ?? 0;
}
} else {
sentProductPrice = stock?.productSalePrice ?? 0;
}
}
return GestureDetector(
onTap: product.productType == ProductType.variant.name
? () async {
if ((product.stocksSumProductStock ?? 0) <= 0) {
EasyLoading.showError(lang.S.of(context).outOfStock);
return;
}
await showAddItemPopup(
mainContext: context,
productModel: product,
ref: ref,
customerType: widget.customerModel?.type,
fromPOSSales: false,
);
}
: () async {
// For Single Products, check stock.
// For Combo, we skip strict stock check here or assume it's allowed.
if (!isCombo && (product.stocksSumProductStock ?? 0) <= 0) {
EasyLoading.showError(lang.S.of(context).outOfStock);
} else {
SaleCartModel cartItem = SaleCartModel(
productName: product.productName,
batchName: '',
stockId: stock?.id ?? 0,
unitPrice: sentProductPrice,
productCode: product.productCode,
productPurchasePrice: stock?.productPurchasePrice,
stock: stock?.productStock,
productType: product.productType,
productId: product.id ?? 0,
quantity: (stock?.productStock ?? 0) < 1
? (isCombo
? 1
: (stock?.productStock ?? 10)) // Ensure combo adds at least 1
: 1,
);
providerData.addToCartRiverPod(cartItem: cartItem, fromEditSales: false);
Navigator.pop(context);
}
},
child: ProductCard(
productTitle: product.productName.toString(),
productPrice: sentProductPrice,
productImage: product.productPicture,
stockInfo: stockDisplayText, // Passing String instead of num
),
);
},
);
},
error: (e, stack) {
return Text('Error: ${e.toString()}');
},
loading: () {
return const Center(child: CircularProgressIndicator());
},
),
],
),
),
),
);
}),
);
}
}
// ignore: must_be_immutable
class ProductCard extends StatefulWidget {
ProductCard({
super.key,
required this.productTitle,
required this.productPrice,
required this.productImage,
required this.stockInfo, // Changed from 'num stock' to 'String stockInfo'
});
String productTitle;
num productPrice;
String stockInfo; // Type changed
String? productImage;
@override
State<ProductCard> createState() => _ProductCardState();
}
class _ProductCardState extends State<ProductCard> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Consumer(builder: (context, ref, __) {
// Removed the quantity calculation loop here as it wasn't being used in the UI directly
// If you need to show current cart quantity on the card, let me know.
// final permissionService = PermissionService(ref); // Uncomment if permission needed
return Padding(
padding: const EdgeInsets.all(5.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
children: [
Padding(
padding: const EdgeInsets.all(4.0),
child: Container(
height: 50,
width: 50,
decoration: widget.productImage == null
? BoxDecoration(
image: DecorationImage(image: AssetImage(noProductImageUrl), fit: BoxFit.cover),
borderRadius: BorderRadius.circular(90.0),
)
: BoxDecoration(
image: DecorationImage(
image: NetworkImage("${APIConfig.domain}${widget.productImage}"), fit: BoxFit.cover),
borderRadius: BorderRadius.circular(90.0),
),
),
),
Flexible(
child: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.productTitle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium!.copyWith(fontSize: 18),
),
// Display the stockInfo string (Either "Combo" or "Stock: 50")
Text(
widget.stockInfo,
style: const TextStyle(color: Colors.grey),
),
],
),
),
),
],
),
),
// if (permissionService.hasPermission(Permit.salesPriceView.value))
Text(
'$currency${widget.productPrice}',
style: theme.textTheme.titleMedium!.copyWith(fontSize: 18),
),
],
),
);
});
}
}