import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:marco/controller/finance/payment_request_detail_controller.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/model/expense/employee_selector_bottom_sheet.dart'; class UpdatePaymentRequestWithReimbursement extends StatefulWidget { final String expenseId; final String statusId; final void Function() onClose; const UpdatePaymentRequestWithReimbursement({ super.key, required this.expenseId, required this.onClose, required this.statusId, }); @override State createState() => _UpdatePaymentRequestWithReimbursementState(); } class _UpdatePaymentRequestWithReimbursementState extends State { final PaymentRequestDetailController controller = Get.find(); final TextEditingController commentCtrl = TextEditingController(); final TextEditingController txnCtrl = TextEditingController(); final TextEditingController tdsCtrl = TextEditingController(text: '0'); final TextEditingController baseAmountCtrl = TextEditingController(); final TextEditingController taxAmountCtrl = TextEditingController(); final RxString dateStr = ''.obs; final RxDouble tdsAmount = 0.0.obs; final RxDouble netPayable = 0.0.obs; @override void initState() { super.initState(); baseAmountCtrl.addListener(_recalculateTds); taxAmountCtrl.addListener(_recalculateTds); tdsCtrl.addListener(_recalculateTds); } @override void dispose() { commentCtrl.dispose(); txnCtrl.dispose(); tdsCtrl.dispose(); baseAmountCtrl.dispose(); taxAmountCtrl.dispose(); super.dispose(); } void _recalculateTds() { final double base = double.tryParse(baseAmountCtrl.text.trim()) ?? 0.0; final double gst = double.tryParse(taxAmountCtrl.text.trim()) ?? 0.0; final double tdsPercent = double.tryParse(tdsCtrl.text.trim()) ?? 0.0; final double calculatedTds = (base * tdsPercent) / 100; final double roundedTds = double.parse(calculatedTds.toStringAsFixed(2)); final double net = (base + gst) - roundedTds; final double roundedNet = double.parse(net.toStringAsFixed(2)); tdsAmount.value = roundedTds; netPayable.value = roundedNet; } void _showEmployeeList() async { await showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), backgroundColor: Colors.transparent, builder: (_) => ReusableEmployeeSelectorBottomSheet( searchController: controller.employeeSearchController, searchResults: controller.employeeSearchResults, isSearching: controller.isSearchingEmployees, onSearch: controller.searchEmployees, onSelect: (emp) => controller.selectedReimbursedBy.value = emp, ), ); controller.employeeSearchController.clear(); controller.employeeSearchResults.clear(); } InputDecoration _inputDecoration(String hint) { return InputDecoration( hintText: hint, hintStyle: MyTextStyle.bodySmall(xMuted: true), filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5), ), contentPadding: MySpacing.all(16), ); } Widget _requiredLabel(String label) { return RichText( text: TextSpan( text: label, style: MyTextStyle.labelMedium(), children: const [ TextSpan( text: ' *', style: TextStyle(color: Colors.red), ), ], ), ); } Widget _readOnlyValueBox(String label, String value, Color color) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), decoration: BoxDecoration( color: color.withOpacity(0.1), border: Border.all(color: color.withOpacity(0.3)), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ MyText.labelMedium(label), MyText.bodyMedium( "₹$value", color: color, fontWeight: 700, ), ], ), ); } @override Widget build(BuildContext context) { return Obx(() { return BaseBottomSheet( title: "Proceed Payment", isSubmitting: controller.isLoading.value, onCancel: () { widget.onClose(); Navigator.pop(context); }, onSubmit: () async { if (txnCtrl.text.trim().isEmpty || dateStr.value.isEmpty || baseAmountCtrl.text.trim().isEmpty || taxAmountCtrl.text.trim().isEmpty || commentCtrl.text.trim().isEmpty || controller.selectedReimbursedBy.value == null) { showAppSnackbar( title: "Incomplete", message: "Please fill all mandatory fields, including 'Paid By'", type: SnackbarType.warning, ); return; } try { final parsedDate = DateFormat('dd-MM-yyyy').parse(dateStr.value, true); final baseAmt = double.tryParse(baseAmountCtrl.text.trim()) ?? 0; final taxAmt = double.tryParse(taxAmountCtrl.text.trim()) ?? 0; final tdsPercentage = tdsCtrl.text.trim().isEmpty ? null : tdsCtrl.text.trim(); final success = await controller.updatePaymentRequestStatus( statusId: widget.statusId, comment: commentCtrl.text.trim(), paidTransactionId: txnCtrl.text.trim(), paidById: controller.selectedReimbursedBy.value?.id, paidAt: parsedDate, baseAmount: baseAmt, taxAmount: taxAmt, tdsPercentage: tdsPercentage, ); showAppSnackbar( title: success ? 'Success' : 'Error', message: success ? 'Payment updated successfully' : 'Failed to update payment', type: success ? SnackbarType.success : SnackbarType.error, ); if (success) { widget.onClose(); if (Navigator.canPop(context)) { Navigator.pop(context); } else { Get.close(1); } } } catch (e) { showAppSnackbar( title: 'Error', message: 'Something went wrong. Please try again.', type: SnackbarType.error, ); } }, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _requiredLabel("Transaction ID"), MySpacing.height(8), TextField( controller: txnCtrl, decoration: _inputDecoration("Enter transaction ID"), ), MySpacing.height(16), _requiredLabel("Transaction Date"), MySpacing.height(8), GestureDetector( onTap: () async { final DateTime submittedDate = controller.paymentRequest.value?.createdAt ?? DateTime.now(); final DateTime today = DateTime.now(); final picked = await showDatePicker( context: context, initialDate: today.isBefore(submittedDate) ? submittedDate : today, firstDate: submittedDate, lastDate: today, helpText: 'Select Transaction Date', ); if (picked != null) { dateStr.value = DateFormat('dd-MM-yyyy').format(picked); } }, child: AbsorbPointer( child: TextField( controller: TextEditingController(text: dateStr.value), decoration: _inputDecoration("Select Date").copyWith( suffixIcon: const Icon(Icons.calendar_today), ), ), ), ), MySpacing.height(16), _requiredLabel("Paid By"), MySpacing.height(8), GestureDetector( onTap: _showEmployeeList, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade300), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( controller.selectedReimbursedBy.value == null ? "Select Paid By" : '${controller.selectedReimbursedBy.value?.firstName ?? ''} ${controller.selectedReimbursedBy.value?.lastName ?? ''}', style: const TextStyle(fontSize: 14), ), const Icon(Icons.arrow_drop_down, size: 22), ], ), ), ), MySpacing.height(16), _requiredLabel("Base Amount"), MySpacing.height(8), TextField( controller: baseAmountCtrl, keyboardType: TextInputType.number, decoration: _inputDecoration("Enter Base Amount"), ), MySpacing.height(16), _requiredLabel("GST Amount"), MySpacing.height(8), TextField( controller: taxAmountCtrl, keyboardType: TextInputType.number, decoration: _inputDecoration("Enter GST Amount"), ), MySpacing.height(16), MyText.labelMedium("TDS Percent"), MySpacing.height(8), TextField( controller: tdsCtrl, keyboardType: TextInputType.number, decoration: _inputDecoration("Enter TDS Percent").copyWith( suffixIcon: Padding( padding: const EdgeInsets.only(right: 12), child: Icon(Icons.percent, size: 20, color: Colors.grey.shade600), ), suffixIconConstraints: const BoxConstraints(minWidth: 0, minHeight: 0), ), onChanged: (_) => _recalculateTds(), ), MySpacing.height(4), MyText.bodySmall( "TDS is applied on base amount only.", color: Colors.grey.shade600, ), MySpacing.height(16), Obx(() => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _readOnlyValueBox( "TDS Amount", tdsAmount.value.toStringAsFixed(2), Colors.orange, ), MySpacing.height(12), _readOnlyValueBox( "Net Payable", netPayable.value.toStringAsFixed(2), Colors.green, ), ], )), MySpacing.height(20), _requiredLabel("Comment"), MySpacing.height(8), TextField( controller: commentCtrl, maxLines: 2, decoration: _inputDecoration("Enter comment"), ), ], ), ), ); }); } }