import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:on_field_work/controller/expense/expense_detail_controller.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:on_field_work/helpers/widgets/my_text_style.dart'; import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/model/expense/employee_selector_bottom_sheet.dart'; class ReimbursementBottomSheet extends StatefulWidget { final String expenseId; final String statusId; final void Function() onClose; final Future Function({ required String comment, required String reimburseTransactionId, required String reimburseDate, required String reimburseById, required String statusId, required double baseAmount, required double taxAmount, required double tdsPercent, required double netPayable, }) onSubmit; const ReimbursementBottomSheet({ super.key, required this.expenseId, required this.onClose, required this.onSubmit, required this.statusId, }); @override State createState() => _ReimbursementBottomSheetState(); } class _ReimbursementBottomSheetState extends State { final ExpenseDetailController controller = Get.find(); final TextEditingController commentCtrl = TextEditingController(); final TextEditingController txnCtrl = TextEditingController(); final TextEditingController baseAmountCtrl = TextEditingController(); final TextEditingController gstAmountCtrl = TextEditingController(); final TextEditingController tdsCtrl = TextEditingController(text: '0'); final RxString dateStr = ''.obs; final RxDouble tdsAmount = 0.0.obs; final RxDouble netPayable = 0.0.obs; @override void initState() { super.initState(); baseAmountCtrl.addListener(_recalculate); gstAmountCtrl.addListener(_recalculate); tdsCtrl.addListener(_recalculate); } @override void dispose() { commentCtrl.dispose(); txnCtrl.dispose(); baseAmountCtrl.dispose(); gstAmountCtrl.dispose(); tdsCtrl.dispose(); super.dispose(); } void _recalculate() { final double base = double.tryParse(baseAmountCtrl.text.trim()) ?? 0.0; final double gst = double.tryParse(gstAmountCtrl.text.trim()) ?? 0.0; final double tdsPercent = double.tryParse(tdsCtrl.text.trim()) ?? 0.0; final double calculatedTds = (base * tdsPercent) / 100; final double net = (base + gst) - calculatedTds; tdsAmount.value = double.parse(calculatedTds.toStringAsFixed(2)); netPayable.value = double.parse(net.toStringAsFixed(2)); } 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 _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: "Reimbursement Info", isSubmitting: controller.isLoading.value, onCancel: () { widget.onClose(); Navigator.pop(context); }, onSubmit: () async { final expenseTransactionDateStr = controller.expense.value?.transactionDate; DateTime? expenseTransactionDate; if (expenseTransactionDateStr != null) { try { expenseTransactionDate = DateTime.parse(expenseTransactionDateStr); } catch (_) {} } if (commentCtrl.text.trim().isEmpty || dateStr.value.isEmpty || controller.selectedReimbursedBy.value == null || baseAmountCtrl.text.trim().isEmpty || gstAmountCtrl.text.trim().isEmpty) { showAppSnackbar( title: "Incomplete", message: "Please fill all required fields", type: SnackbarType.warning, ); return; } // Validate reimbursement date final DateTime? selectedDate = DateTime.tryParse(dateStr.value); if (selectedDate != null) { final now = DateTime.now(); if (selectedDate.isAfter(now)) { showAppSnackbar( title: "Invalid Date", message: "Reimbursement date cannot be in the future.", type: SnackbarType.warning, ); return; } if (expenseTransactionDate != null && selectedDate != null) { final normalizedSelected = DateTime( selectedDate.year, selectedDate.month, selectedDate.day, ); final normalizedTransaction = DateTime( expenseTransactionDate.year, expenseTransactionDate.month, expenseTransactionDate.day, ); if (normalizedSelected.isBefore(normalizedTransaction)) { showAppSnackbar( title: "Invalid Date", message: "Reimbursement date cannot be before the transaction date (${DateFormat('yyyy-MM-dd').format(expenseTransactionDate)}).", type: SnackbarType.warning, ); return; } } } final success = await widget.onSubmit( comment: commentCtrl.text.trim(), reimburseTransactionId: txnCtrl.text.trim(), reimburseDate: dateStr.value, reimburseById: controller.selectedReimbursedBy.value!.id, statusId: widget.statusId, baseAmount: double.tryParse(baseAmountCtrl.text.trim()) ?? 0, taxAmount: double.tryParse(gstAmountCtrl.text.trim()) ?? 0, tdsPercent: double.tryParse(tdsCtrl.text.trim()) ?? 0, netPayable: netPayable.value, ); if (success) { Get.back(); showAppSnackbar( title: "Success", message: "Reimbursement submitted successfully", type: SnackbarType.success, ); } else { showAppSnackbar( title: "Error", message: controller.errorMessage.value, type: SnackbarType.error, ); } }, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.labelMedium("Transaction ID*"), MySpacing.height(8), TextField( controller: txnCtrl, decoration: _inputDecoration("Enter transaction ID"), ), MySpacing.height(16), MyText.labelMedium("Reimbursement Date*"), MySpacing.height(8), GestureDetector( onTap: () async { // Get transaction date from expense DateTime? transactionDate; if (controller.expense.value?.transactionDate != null) { try { transactionDate = DateTime.parse( controller.expense.value!.transactionDate); } catch (_) { transactionDate = null; } } final DateTime now = DateTime.now(); final DateTime firstDate = transactionDate ?? DateTime(2020); // fallback if null final DateTime lastDate = now; final picked = await showDatePicker( context: context, initialDate: now.isBefore(firstDate) ? firstDate : now, // initial date inside the range firstDate: firstDate, lastDate: lastDate, ); if (picked != null) { dateStr.value = DateFormat('yyyy-MM-dd').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), MyText.labelMedium("Reimbursed 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), MyText.labelMedium("Base Amount*"), MySpacing.height(8), TextField( controller: baseAmountCtrl, keyboardType: TextInputType.number, decoration: _inputDecoration("Enter Base Amount"), ), MySpacing.height(16), MyText.labelMedium("GST Amount*"), MySpacing.height(8), TextField( controller: gstAmountCtrl, 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: (_) => _recalculate(), ), 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(16), MyText.labelMedium("Comment*"), MySpacing.height(8), TextField( controller: commentCtrl, maxLines: 2, decoration: _inputDecoration("Enter comment"), ), MySpacing.height(16), ], ), ), ); }); } }