first commit
This commit is contained in:
704
lib/Screens/Sales/Repo/sales_repo.dart
Normal file
704
lib/Screens/Sales/Repo/sales_repo.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
1245
lib/Screens/Sales/add_sales.dart
Normal file
1245
lib/Screens/Sales/add_sales.dart
Normal file
File diff suppressed because it is too large
Load Diff
272
lib/Screens/Sales/batch_select_popup_sales.dart
Normal file
272
lib/Screens/Sales/batch_select_popup_sales.dart
Normal 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)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
225
lib/Screens/Sales/provider/sales_cart_provider.dart
Normal file
225
lib/Screens/Sales/provider/sales_cart_provider.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
207
lib/Screens/Sales/sales_add_to_cart_sales_widget.dart
Normal file
207
lib/Screens/Sales/sales_add_to_cart_sales_widget.dart
Normal 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),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
207
lib/Screens/Sales/sales_cart_widget.dart
Normal file
207
lib/Screens/Sales/sales_cart_widget.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
326
lib/Screens/Sales/sales_products_list_screen.dart
Normal file
326
lib/Screens/Sales/sales_products_list_screen.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user