import 'dart:convert'; import 'dart:io'; import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconly/iconly.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:mobile_pos/Screens/Sales/provider/sales_cart_provider.dart'; import 'package:mobile_pos/Provider/profile_provider.dart'; import 'package:mobile_pos/Screens/Sales/Repo/sales_repo.dart'; import 'package:mobile_pos/Screens/Sales/sales_cart_widget.dart'; import 'package:mobile_pos/Screens/Sales/sales_products_list_screen.dart'; import 'package:mobile_pos/Screens/Settings/sales%20settings/model/amount_rounding_dropdown_model.dart'; import 'package:mobile_pos/generated/l10n.dart' as lang; import 'package:nb_utils/nb_utils.dart'; import '../../Const/api_config.dart'; import '../../GlobalComponents/glonal_popup.dart'; import '../../Repository/API/future_invoice.dart'; import '../../constant.dart'; import '../../currency.dart'; import '../../model/add_to_cart_model.dart'; import '../../model/sale_transaction_model.dart'; import '../../widgets/multipal payment mathods/multi_payment_widget.dart'; import '../Customers/Model/parties_model.dart'; import '../Home/home.dart'; import '../../service/check_user_role_permission_provider.dart'; import '../Products/add product/modle/create_product_model.dart'; import '../invoice_details/sales_invoice_details_screen.dart'; import '../vat_&_tax/model/vat_model.dart'; import '../vat_&_tax/provider/text_repo.dart'; class AddSalesScreen extends ConsumerStatefulWidget { AddSalesScreen({ super.key, required this.customerModel, this.transitionModel, this.isFromPos, }); Party? customerModel; final SalesTransactionModel? transitionModel; bool? isFromPos; @override AddSalesScreenState createState() => AddSalesScreenState(); } class AddSalesScreenState extends ConsumerState { // Key to access MultiPaymentWidget State final GlobalKey paymentWidgetKey = GlobalKey(); bool isProcessing = false; DateTime selectedDate = DateTime.now(); TextEditingController dateController = TextEditingController(text: DateTime.now().toString().substring(0, 10)); TextEditingController phoneController = TextEditingController(); TextEditingController recevedAmountController = TextEditingController(); TextEditingController noteController = TextEditingController(); bool _initialingFirstTime = false; @override void initState() { super.initState(); // Listener for Received Amount Controller to calculate prices recevedAmountController.addListener(() { final cart = ref.read(cartNotifier); cart.calculatePrice(receivedAmount: recevedAmountController.text, stopRebuild: !_initialingFirstTime); }); if (widget.transitionModel != null) { final editedSales = widget.transitionModel; dateController.text = editedSales?.saleDate?.substring(0, 10) ?? ''; recevedAmountController.text = editedSales?.paidAmount.toString() ?? ''; widget.customerModel = Party( id: widget.transitionModel?.party?.id, name: widget.transitionModel?.party?.name, ); if (widget.transitionModel?.discountType == 'flat') { discountType = 'Flat'; } else { discountType = 'Percent'; } // Note: Pre-populating multi-payment from edit model would require parsing editedSales.paymentType or similar addProductsInCartFromEditList(); } _initialingFirstTime = true; } @override void dispose() { dateController.dispose(); phoneController.dispose(); recevedAmountController.dispose(); super.dispose(); } void addProductsInCartFromEditList() { final cart = ref.read(cartNotifier); cart.roundedOption = widget.transitionModel?.roundingOption ?? roundingMethods[0].value; if (widget.transitionModel?.salesDetails?.isNotEmpty ?? false) { for (var detail in widget.transitionModel!.salesDetails!) { SaleCartModel cartItem = SaleCartModel( productType: detail.product?.productType, productName: detail.product?.productName, discountAmount: detail.discount, unitPrice: detail.price, batchName: detail.stock?.batchNo ?? '', lossProfit: detail.lossProfit, quantity: detail.quantities ?? 0, productCode: detail.product?.productCode, productPurchasePrice: detail.product?.productPurchasePrice, stock: detail.stock?.productCurrentStock, productId: detail.productId!, stockId: detail.stock?.id ?? 0); cart.addToCartRiverPod( cartItem: cartItem, fromEditSales: true, isVariant: detail.product?.productType == ProductType.variant.name); } } cart.discountAmount = widget.transitionModel?.discountAmount ?? 0; noteController.text = widget.transitionModel?.meta?.note?.toString() ?? ''; if (widget.transitionModel?.discountType == 'flat') { cart.discountTextControllerFlat.text = widget.transitionModel?.discountAmount.toString() ?? ''; } else { cart.discountTextControllerFlat.text = widget.transitionModel?.discountPercent?.toString() ?? ''; } cart.finalShippingCharge = widget.transitionModel?.shippingCharge ?? 0; cart.shippingChargeController.text = widget.transitionModel?.shippingCharge.toString() ?? ''; cart.vatAmountController.text = widget.transitionModel?.vatAmount.toString() ?? ''; cart.calculatePrice(receivedAmount: widget.transitionModel?.paidAmount.toString(), stopRebuild: true); } bool hasPreselected = false; // Flag to ensure preselection happens only once String flatValue = 'Flat'; String percentValue = 'Percent'; String discountType = 'Flat'; File? _imageFile; Future _pickImage(ImageSource source) async { final pickedFile = await ImagePicker().pickImage(source: source); setState(() { if (pickedFile != null) { _imageFile = File(pickedFile.path); } else { print('No image selected.'); } }); } @override Widget build(BuildContext context) { final _theme = Theme.of(context); double _height = 100; final providerData = ref.watch(cartNotifier); final personalData = ref.watch(businessInfoProvider); final taxesData = ref.watch(taxProvider); final permissionService = PermissionService(ref); return personalData.when(data: (data) { return GlobalPopup( child: Scaffold( backgroundColor: kWhite, appBar: AppBar( backgroundColor: Colors.white, title: Text( lang.S.of(context).addSales, ), centerTitle: true, iconTheme: const IconThemeData(color: Colors.black), elevation: 2.0, surfaceTintColor: kWhite, ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(20.0), child: Column( children: [ ///_______Invoice_And_Date_____________________________________________________ Row( children: [ widget.transitionModel == null ? FutureBuilder( future: FutureInvoice().getFutureInvoice(tag: 'sales'), builder: (context, snapshot) { if (snapshot.hasData) { final invoiceValue = (snapshot.data != null) ? snapshot.data.toString().replaceAll('"', '') : ''; return Expanded( child: AppTextField( textFieldType: TextFieldType.NAME, initialValue: invoiceValue ?? '', readOnly: true, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: lang.S.of(context).inv, border: const OutlineInputBorder(), ), ), ); } else { return Expanded( child: TextFormField( readOnly: true, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: lang.S.of(context).inv, border: const OutlineInputBorder(), ), ), ); } }, ) : Expanded( child: AppTextField( textFieldType: TextFieldType.NAME, initialValue: widget.transitionModel?.invoiceNumber, readOnly: true, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: lang.S.of(context).inv, border: const OutlineInputBorder(), ), ), ), const SizedBox(width: 16), Expanded( child: TextFormField( readOnly: true, controller: dateController, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: lang.S.of(context).date, suffixIconConstraints: const BoxConstraints( minWidth: 20, minHeight: 20, ), suffixIcon: IconButton( // padding: EdgeInsets.zero, visualDensity: VisualDensity(horizontal: -4, vertical: -4), onPressed: () async { final DateTime? picked = await showDatePicker( initialDate: selectedDate, firstDate: DateTime(2015, 8), lastDate: DateTime(2101), context: context, ); if (picked != null && picked != selectedDate) { setState(() { selectedDate = selectedDate.copyWith( year: picked.year, month: picked.month, day: picked.day, ); dateController.text = selectedDate.toString().substring(0, 10); }); } }, icon: Icon( IconlyLight.calendar, color: kPeraColor, ), ), ), ), ), ], ), ///______Selected_Due_And_Customer___________________________________________ const SizedBox(height: 20), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Text(lang.S.of(context).dueAmount), Text( widget.customerModel?.due == null ? '$currency 0' : '$currency${widget.customerModel?.due}', style: const TextStyle(color: Color(0xFFFF8C34)), ), ], ), const SizedBox( height: 10, ), AppTextField( textFieldType: TextFieldType.NAME, readOnly: true, initialValue: widget.customerModel?.name ?? 'Guest', decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: lang.S.of(context).customerName, border: const OutlineInputBorder(), ), ), Visibility( visible: widget.customerModel == null, child: Padding( padding: const EdgeInsets.only(top: 20.0), child: AppTextField( controller: phoneController, textFieldType: TextFieldType.PHONE, decoration: kInputDecoration.copyWith( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: lang.S.of(context).customerPhoneNumber, hintText: lang.S.of(context).enterCustomerPhoneNumber, ), ), ), ), ], ), SizedBox(height: 12), ///_______Add_Button__________________________________________________ if (widget.isFromPos != true) ElevatedButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => SaleProductsList( customerModel: widget.customerModel, ), ), ); }, style: ElevatedButton.styleFrom( elevation: 0.0, backgroundColor: kMainColor2, minimumSize: Size.fromHeight(40), ), child: Text(lang.S.of(context).addItems, style: _theme.textTheme.titleMedium?.copyWith( color: kMainColor, )), ), const SizedBox(height: 12), ///_______Added_Items_List_________________________________________________ SalesCartListWidget(), ///_____Total_Section_____________________________ Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(10)), border: Border.all(color: Colors.grey.shade300, width: 1)), child: Column( children: [ ///________Total_title_reader_________________________ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: kMainColor2, borderRadius: BorderRadius.only(topRight: Radius.circular(10), topLeft: Radius.circular(10))), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( lang.S.of(context).subTotal, style: _theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, ), ), Text( '$currency${formatPointNumber(providerData.totalAmount)}', style: _theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, ), ), ], ), ), ///_________Discount___________________________________ Padding( padding: const EdgeInsets.only(right: 10, left: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ // Text for "Discount" Text( lang.S.of(context).discount, style: _theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, color: kPeraColor, ), ), Spacer(), SizedBox( width: context.width() / 4, height: 30, child: Container( decoration: const BoxDecoration( border: Border(bottom: BorderSide(color: kBorder, width: 1)), ), child: DropdownButton( icon: const Icon( Icons.keyboard_arrow_down, color: kPeraColor, size: 18, ), dropdownColor: Colors.white, isExpanded: true, isDense: true, padding: EdgeInsets.zero, hint: Text( lang.S.of(context).select, style: _theme.textTheme.bodyMedium?.copyWith( color: kGreyTextColor, ), ), value: discountType, items: [ DropdownMenuItem( value: flatValue, child: Text(lang.S.of(context).flat), ), DropdownMenuItem( value: percentValue, child: Text(lang.S.of(context).percent), ), ], onChanged: (value) { setState(() { discountType = value!; providerData.calculateDiscount( value: providerData.discountTextControllerFlat.text, selectedTaxType: discountType, ); }); }, ), ), ), const SizedBox(width: 10), SizedBox( width: context.width() / 4, height: 30, child: TextFormField( style: _theme.textTheme.titleSmall, controller: providerData.discountTextControllerFlat, onChanged: (value) { setState(() { providerData.calculateDiscount( value: value, selectedTaxType: discountType, ); }); }, textAlign: TextAlign.right, decoration: InputDecoration( hintText: '0', hintStyle: _theme.textTheme.titleMedium?.copyWith( color: kPeraColor, ), border: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)), enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)), focusedBorder: UnderlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 8), ), keyboardType: TextInputType.number, ), ), ], ), ), ///_________Vat_Dropdown_______________________________ Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( lang.S.of(context).vat, style: _theme.textTheme.titleSmall?.copyWith( color: kPeraColor, ), ), const SizedBox(width: 10), const Spacer(), taxesData.when( data: (data) { List dataList = data.where((tax) => tax.status == true).toList(); if (widget.transitionModel != null && widget.transitionModel?.vatId != null && !hasPreselected) { VatModel matched = dataList.firstWhere( (element) => element.id == widget.transitionModel?.vatId, orElse: () => VatModel(), ); if (matched.id != null) { hasPreselected = true; providerData.selectedVat = matched; } } return SizedBox( width: context.width() / 4, height: 30, child: Container( decoration: const BoxDecoration( border: Border( bottom: BorderSide(color: kBorder, width: 1), ), ), child: DropdownButton( icon: providerData.selectedVat != null ? GestureDetector( onTap: () => providerData.changeSelectedVat(data: null), child: const Icon( Icons.close, color: Colors.red, size: 16, ), ) : const Icon( Icons.keyboard_arrow_down, color: kPeraColor, size: 18, ), dropdownColor: Colors.white, isExpanded: true, isDense: true, padding: EdgeInsets.zero, hint: Text( lang.S.of(context).selectOne, style: _theme.textTheme.bodyMedium?.copyWith( color: kPeraColor, ), ), value: providerData.selectedVat, items: dataList.map((VatModel tax) { return DropdownMenuItem( value: tax, child: Text( tax.name ?? '', maxLines: 1, style: _theme.textTheme.bodyMedium?.copyWith( color: kPeraColor, ), ), ); }).toList(), onChanged: (VatModel? newValue) => providerData.changeSelectedVat(data: newValue), ), ), ); }, error: (error, stackTrace) { return Text(error.toString()); }, loading: () { return const SizedBox.shrink(); }, ), const SizedBox(width: 10), // VAT Amount Input Field SizedBox( height: 30, width: 100, child: TextFormField( controller: providerData.vatAmountController, style: _theme.textTheme.titleSmall, readOnly: true, onChanged: (value) => providerData.calculateDiscount( value: value, selectedTaxType: discountType.toString(), ), textAlign: TextAlign.right, decoration: InputDecoration( hintText: '0', hintStyle: _theme.textTheme.titleSmall?.copyWith( color: kPeraColor, ), border: const UnderlineInputBorder(borderSide: BorderSide(color: kBorder)), enabledBorder: const UnderlineInputBorder(borderSide: BorderSide(color: kBorder)), focusedBorder: const UnderlineInputBorder(), contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 8), ), keyboardType: TextInputType.number, ), ), ], ), ), Padding( padding: const EdgeInsets.only(right: 10, left: 10, top: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( lang.S.of(context).shippingCharge, style: _theme.textTheme.titleSmall?.copyWith( color: kPeraColor, ), ), SizedBox( width: context.width() / 4, height: 30, child: TextFormField( controller: providerData.shippingChargeController, keyboardType: TextInputType.number, onChanged: (value) => providerData.calculatePrice(shippingCharge: value, stopRebuild: false), textAlign: TextAlign.right, style: _theme.textTheme.titleSmall, decoration: InputDecoration( hintText: '0', hintStyle: _theme.textTheme.titleSmall?.copyWith( color: kPeraColor, ), border: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)), enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)), focusedBorder: UnderlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 8), ), ), ), ], ), ), ///________Total_______________________________________ Padding( padding: const EdgeInsets.only(right: 10, left: 10, top: 7), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( lang.S.of(context).total, style: _theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), Text( formatPointNumber(providerData.actualTotalAmount), style: _theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), ], ), ), ///________Rounded Total_______________________________________ Visibility( // visible: providerData.roundingAmount != 0, child: Column( children: [ ///________Rounded Amount_______________________________________ Padding( padding: const EdgeInsets.only(right: 10, left: 10, top: 7), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( lang.S.of(context).roundings, style: _theme.textTheme.titleSmall?.copyWith( color: kPeraColor, ), ), Text( formatPointNumber(providerData.roundingAmount), style: _theme.textTheme.titleSmall?.copyWith( color: kPeraColor, ), ), ], ), ), Padding( padding: const EdgeInsets.only(right: 10, left: 10, top: 7), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( lang.S.of(context).roundingTotal, style: _theme.textTheme.titleSmall?.copyWith( color: kPeraColor, ), ), Text( formatPointNumber(providerData.totalPayableAmount), style: _theme.textTheme.titleSmall?.copyWith( color: kPeraColor, ), ), ], ), ), ], ), ), ///________paid_Amount__________________________________ Padding( padding: const EdgeInsets.only(right: 10, left: 10, top: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( lang.S.of(context).receivedAmount, style: _theme.textTheme.titleSmall?.copyWith( color: kPeraColor, ), ), SizedBox( width: context.width() / 4, height: 30, child: TextFormField( controller: recevedAmountController, readOnly: (paymentWidgetKey.currentState?.getPaymentEntries().length ?? 1) > 1, keyboardType: TextInputType.number, textAlign: TextAlign.right, style: _theme.textTheme.titleSmall, decoration: InputDecoration( hintText: '0', hintStyle: _theme.textTheme.titleSmall?.copyWith( color: kPeraColor, ), border: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)), enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)), focusedBorder: UnderlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 8), ), ), ), ], ), ), ///________Change amount_________________________________ Visibility( visible: providerData.changeAmount > 0, child: Padding( padding: const EdgeInsets.only(right: 10, left: 10, top: 13, bottom: 13), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( lang.S.of(context).changeAmount, style: _theme.textTheme.titleSmall?.copyWith( color: kPeraColor, ), ), Text( formatPointNumber(providerData.changeAmount), style: _theme.textTheme.titleSmall?.copyWith( color: kPeraColor, ), ), ], ), ), ), ///_______Due_amount_____________________________________ Visibility( visible: providerData.dueAmount > 0 || (providerData.changeAmount == 0 && providerData.dueAmount == 0), child: Padding( padding: const EdgeInsets.only(right: 10, left: 10, top: 13, bottom: 13), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( lang.S.of(context).dueAmount, style: _theme.textTheme.titleSmall?.copyWith( color: kPeraColor, ), ), Text( formatPointNumber(providerData.dueAmount), style: _theme.textTheme.titleSmall?.copyWith( color: kPeraColor, ), ), ], ), ), ), ], ), ), const SizedBox(height: 20), ///_______Payment_Type_______________________________ // REPLACED: PaymentTypeSelectorDropdown MultiPaymentWidget( key: paymentWidgetKey, showWalletOption: true, totalAmountController: recevedAmountController, showChequeOption: true, initialTransactions: widget.transitionModel?.transactions, onPaymentListChanged: () { providerData.calculatePrice(receivedAmount: recevedAmountController.text); }, ), const SizedBox(height: 20), SizedBox( height: 56, // Set a fixed height for the Row child: Row( mainAxisSize: MainAxisSize.min, children: [ Expanded( // Use Expanded to allow the TextFormField to take available space child: Container( constraints: const BoxConstraints( maxHeight: 200, ), child: TextFormField( controller: noteController, maxLines: null, decoration: InputDecoration( hintText: lang.S.of(context).opinion, ), onChanged: (text) { setState(() { _height = (text.split('\n').length * 24).toDouble(); }); }, style: _theme.textTheme.bodyMedium?.copyWith(height: 1.5), ), ), ), const SizedBox(width: 10), _imageFile == null ? widget.transitionModel?.image?.isNotEmpty ?? false ? InkWell( onTap: () { showImagePickerDialog(context, _theme.textTheme); }, child: Container( constraints: const BoxConstraints( maxHeight: 48, minHeight: 48, maxWidth: 107, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), color: const Color(0xffF5F3F3), image: DecorationImage( image: NetworkImage( '${APIConfig.domain}${widget.transitionModel?.image.toString()}', ), fit: BoxFit.contain), ), ), ) : InkWell( onTap: () { showImagePickerDialog(context, _theme.textTheme); }, child: Container( constraints: const BoxConstraints( maxHeight: 48, minHeight: 48, maxWidth: 107, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), color: const Color(0xffF5F3F3), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(IconlyLight.camera), SizedBox(width: 4.0), Text(lang.S.of(context).image), ], ), ), ) : InkWell( onTap: () { showImagePickerDialog(context, _theme.textTheme); }, child: Container( constraints: const BoxConstraints( maxHeight: 48, minHeight: 48, maxWidth: 107, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), image: DecorationImage( image: FileImage(_imageFile!), fit: BoxFit.cover, ), ), ), ), ], ), ), ///_____Action_Button_____________________________________ const SizedBox(height: 24), Row( children: [ Expanded( child: OutlinedButton( style: OutlinedButton.styleFrom( maximumSize: const Size(double.infinity, 48), minimumSize: const Size(double.infinity, 48), disabledBackgroundColor: _theme.colorScheme.primary.withValues(alpha: 0.15), ), onPressed: () async { const Home().launch(context, isNewTask: true); }, child: Text( lang.S.of(context).cancel, maxLines: 1, overflow: TextOverflow.ellipsis, style: _theme.textTheme.bodyMedium?.copyWith( color: _theme.colorScheme.primary, fontWeight: FontWeight.w600, fontSize: 16, ), ), ), ), const SizedBox(width: 20), Expanded( child: ElevatedButton( style: ElevatedButton.styleFrom( maximumSize: const Size(double.infinity, 48), minimumSize: const Size(double.infinity, 48), backgroundColor: isProcessing ? _theme.colorScheme.primary.withOpacity(0.15) : _theme.colorScheme.primary, ), onPressed: () async { if (providerData.cartItemList.isEmpty) { EasyLoading.showError(lang.S.of(context).addProductFirst); return; } if (widget.customerModel == null && providerData.dueAmount > 0) { EasyLoading.showError( lang.S.of(context).dueSaleWarn, ); return; } // Validate Payments from the Widget List payments = paymentWidgetKey.currentState?.getPaymentEntries() ?? []; if (payments.isEmpty) { EasyLoading.showError('Please select at least one payment method'); return; } // Basic validation for each entry // for (var p in payments) { // if (p.type == null) { // EasyLoading.showError('Please select payment type for all entries'); // return; // } // if (p.amountController.text.isEmpty || (double.tryParse(p.amountController.text) ?? 0) < 0) { // EasyLoading.showError('Invalid amount in payment entries'); // return; // } // } if (isProcessing) return; setState(() { isProcessing = true; }); try { EasyLoading.show( status: lang.S.of(context).loading, dismissOnTap: false, ); // Prepare the list of selected products List selectedProductList = providerData.cartItemList.map((element) { return CartSaleProducts( productName: element.productName ?? '', stockId: element.stockId, quantities: element.quantity, price: num.tryParse(element.unitPrice.toString()) ?? 0, productId: element.productId, discount: element.discountAmount, ); }).toList(); // Prepare image file File? imageFile; if (_imageFile != null) { final file = File(_imageFile!.path); if (await file.exists()) { imageFile = file; } } SaleRepo repo = SaleRepo(); // Serialize Payment List for API (Assuming Repo expects a JSON string in paymentType or similar) // If Repo is not updated to handle this, this is how we pass the data for now. List> paymentData = payments.map((e) => e.toJson()).toList(); String paymentTypeData = jsonEncode(paymentData); if (widget.transitionModel == null) { // Create Sale if (!permissionService.hasPermission(Permit.salesCreate.value)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: Colors.red, content: Text( lang.S.of(context).createSaleWarn, ), ), ); return; } SalesTransactionModel? saleData = await repo.createSale( ref: ref, context: context, totalAmount: providerData.totalPayableAmount, purchaseDate: selectedDate.toIso8601String(), products: selectedProductList, paymentType: paymentTypeData, // Passing serialized payment data partyId: widget.customerModel?.id, customerPhone: widget.customerModel == null ? phoneController.text : null, vatAmount: providerData.vatAmount, vatPercent: providerData.selectedVat?.rate ?? 0, vatId: providerData.selectedVat?.id, isPaid: providerData.isFullPaid, dueAmount: providerData.dueAmount, discountAmount: providerData.discountAmount, changeAmount: providerData.changeAmount, discountType: discountType.toLowerCase() ?? '', roundedOption: providerData.roundedOption, roundingAmount: providerData.roundingAmount, unRoundedTotalAmount: providerData.actualTotalAmount, note: noteController.text, shippingCharge: providerData.finalShippingCharge, image: imageFile, discountPercent: providerData.discountPercent, ); if (saleData != null && personalData.value != null) { final refreshed = await repo.getSingleSale((saleData.id ?? 0).toInt()); if (refreshed == null) { SalesInvoiceDetails( businessInfo: personalData.value!, saleTransaction: refreshed!, fromSale: true, ).launch(context); return; } SalesInvoiceDetails( businessInfo: personalData.value!, saleTransaction: refreshed, fromSale: true, ).launch(context); } // if (saleData != null && personalData.value != null) { // SalesInvoiceDetails( // businessInfo: personalData.value!, // saleTransaction: saleData, // fromSale: true, // ).launch(context); // } } else { // Update Sale if (!permissionService.hasPermission(Permit.salesUpdate.value)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: Colors.red, content: Text(lang.S.of(context).updateSaleWarn), ), ); return; } await repo.updateSale( id: widget.transitionModel?.id ?? 0, ref: ref, context: context, roundingAmount: providerData.roundingAmount, totalAmount: providerData.totalPayableAmount, purchaseDate: DateFormat('yyyy-MM-dd HH:mm:ss').format( DateTime.parse( selectedDate.toString(), ), ), products: selectedProductList, paymentType: paymentTypeData, // Passing serialized payment data partyId: widget.transitionModel?.party?.id, roundedOption: providerData.roundedOption, vatAmount: providerData.vatAmount, vatPercent: providerData.selectedVat?.rate ?? 0, vatId: providerData.selectedVat?.id, isPaid: providerData.isFullPaid, dueAmount: providerData.dueAmount, discountAmount: providerData.discountAmount, unRoundedTotalAmount: providerData.actualTotalAmount, changeAmount: providerData.changeAmount, discountType: discountType.toLowerCase(), note: noteController.text, shippingCharge: providerData.finalShippingCharge, image: imageFile, discountPercent: providerData.discountPercent, ); } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString())), ); } finally { EasyLoading.dismiss(); setState(() { isProcessing = false; }); } }, child: isProcessing ? SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, color: _theme.colorScheme.primaryContainer, ), ) : Text( lang.S.of(context).save, maxLines: 1, overflow: TextOverflow.ellipsis, style: _theme.textTheme.bodyMedium?.copyWith( color: _theme.colorScheme.primaryContainer, fontWeight: FontWeight.w600, fontSize: 16, ), ), ), ), ], ), ], ), ), ), ), ); }, error: (e, stack) { return Center( child: Text(e.toString()), ); }, loading: () { return const Center(child: CircularProgressIndicator()); }); } Future showImagePickerDialog(BuildContext context, TextTheme textTheme) { return showCupertinoDialog( context: context, builder: (BuildContext contexts) => BackdropFilter( filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), child: CupertinoAlertDialog( insetAnimationCurve: Curves.bounceInOut, title: Text( lang.S.of(context).uploadImage, textAlign: TextAlign.center, style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), ), actions: [ CupertinoDialogAction( child: Column( children: [ const Icon(IconlyLight.image, size: 30.0), Text( lang.S.of(context).useGallery, textAlign: TextAlign.center, style: textTheme.bodySmall?.copyWith(fontWeight: FontWeight.bold), ) ], ), onPressed: () async { _pickImage(ImageSource.gallery); Future.delayed(const Duration(milliseconds: 100), () { Navigator.pop(context); }); }, ), CupertinoDialogAction( child: Column( children: [ const Icon(IconlyLight.camera, size: 30.0), Text( lang.S.of(context).openCamera, textAlign: TextAlign.center, style: textTheme.bodySmall?.copyWith(fontWeight: FontWeight.bold), ) ], ), onPressed: () async { _pickImage(ImageSource.camera); Future.delayed(const Duration(milliseconds: 100), () { Navigator.pop(context); }); }, ), ], ), ), ); } }