import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:intl/intl.dart'; import 'package:nhance_partner/data/utils/toastNotification.dart'; import '../../../data/utils/Pagination.dart'; import '../../../core/routing/routes.dart'; import '../../../core/services/api_service.dart'; import '../../layouts/main_layout.dart'; import '../../layouts/responsive_layout.dart'; import '../../providers/manager_provider.dart'; import '../../providers/userRoleProvider.dart'; import '../../themes/indicators/export_btn.dart'; import '../../themes/indicators/search_field_theme.dart'; import 'custom_dateRange.dart'; class PayOutDetails extends ConsumerStatefulWidget { const PayOutDetails({super.key}); @override ConsumerState createState() => _PayOutDetailsState(); } class _PayOutDetailsState extends ConsumerState { static final List _commissionInputFormatter = [ FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')), ]; static const int _utrMinLength = 12; static const int _utrMaxLength = 22; /// Alphanumeric only, length validated on submit (12–22). static final RegExp _utrFullValuePattern = RegExp(r'^[a-zA-Z0-9]{12,22}$'); static final List _utrInputFormatter = [ FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9]')), LengthLimitingTextInputFormatter(_utrMaxLength), ]; static bool _isValidUtr(String raw) => _utrFullValuePattern.hasMatch(raw.trim()); // -------------------------- // Invoice Fields // -------------------------- final _formKey = GlobalKey(); final _utrFormKey = GlobalKey(); // final TextEditingController invoiceNoController; DateTime invoiceDate = DateTime.now(); // dynamic selectedAgentId; List? selectedAgentId; dynamic selectedBrokerID; DateTime? policyTillDate = DateTime.now(); dynamic totalPolicies = '0'; dynamic totalCommission = '0'; List> masterPolicies = []; // -------------------------- // Filters // -------------------------- DateTime? fromDate; DateTime? toDate; Map controllers = {}; List tabHeader = [ 'startDate', 'endDate', 'invoiceNo', 'invoiceDate', 'invoiceStatus', 'utrNumber', 'tableSearch', ]; // -------------------------- // Collapse States // -------------------------- bool invoiceCollapsed = false; bool policyCollapsed = false; // -------------------------- // Sample Data (replace with API later) // -------------------------- final List> allPolicies = []; List> filteredPolicies = []; Set selectedPolicies = {}; List> getBrokerData = []; List> filteredBrokerData = []; List> getAgentData = []; List> filteredAgentData = []; bool isLoading = false; bool hasFetchedTableData = false; int currentPage = 1; int itemsPerPage = 10; final Set _inlineSavingIds = {}; /// Per-policy payout editors so [setState] rebuilds do not reset focus/text. final Map _commissionControllers = {}; late ApiService apiService; dynamic managerId; dynamic userId; dynamic roleId; final DateFormat df = DateFormat('dd-MM-yyyy'); @override void initState() { super.initState(); apiService = ApiService(); for (String field in tabHeader) { controllers[field] = TextEditingController(); } Future.microtask(() async { managerId = ref.read(managerIdProvider); roleId = ref.read(userRoleProvider); userId = ref.read(userIdProvider); print('PD =>PayOutDetails => managerId: $managerId'); if (managerId != null) { getAgentList(managerId); } await getBroker(); }); } int _policyRowId(Map p) { return int.tryParse(p["policy_id"]?.toString() ?? "0") ?? 0; } void _rebuildCommissionControllers(List> policies) { for (final c in _commissionControllers.values) { c.dispose(); } _commissionControllers.clear(); for (final p in policies) { final id = _policyRowId(p); if (id == 0) continue; _commissionControllers[id] = TextEditingController( text: (p["commission_amount"] ?? '').toString(), ); } } void _disposeCommissionControllers() { for (final c in _commissionControllers.values) { c.dispose(); } _commissionControllers.clear(); } TextEditingController? _commissionControllerFor(Map p) { final id = _policyRowId(p); if (id == 0) return null; return _commissionControllers[id]; } Widget _commissionAmountField( Map p, TextEditingController? controller, ) { final fieldKey = ValueKey('commission_${p["id"] ?? p["policy_id"]}'); final decoration = const InputDecoration( isDense: true, contentPadding: EdgeInsets.symmetric( horizontal: 6, vertical: 6, ), border: OutlineInputBorder(), ); const fieldStyle = TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF009B77), ); void onChanged(String value) { p["commission_amount"] = value.trim(); _calculateTotalCommission(); } if (controller != null) { return TextFormField( key: fieldKey, controller: controller, textAlign: TextAlign.right, style: fieldStyle, keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: _commissionInputFormatter, decoration: decoration, onChanged: onChanged, onFieldSubmitted: (value) => _updateInlineCommission(p, value), ); } return TextFormField( key: fieldKey, initialValue: (p["commission_amount"] ?? '').toString(), textAlign: TextAlign.center, style: fieldStyle, keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: _commissionInputFormatter, decoration: decoration, onChanged: onChanged, onFieldSubmitted: (value) => _updateInlineCommission(p, value), ); } @override void dispose() { for (final controller in controllers.values) { controller.dispose(); } _disposeCommissionControllers(); super.dispose(); } String _partnerDisplayText(Map item) { if (item['agent_code'] != null && item['agent_name'] != null) { return '${item['agent_code']} - ${item['agent_name']}'; } return (item['agent_name'] ?? '').toString(); } bool _matchesTableSearch(Map item, String query) { if (query.isEmpty) return true; final searchableFields = [ (item['policy_no'] ?? '').toString(), _formatDate(item['issued_date']?.toString() ?? ''), _partnerDisplayText(item), (item['customer_name'] ?? '').toString(), _amountText(item['premium_amount']), (item['premium_amount'] ?? '').toString(), ]; return searchableFields.any( (field) => field.toLowerCase().contains(query), ); } void applyTableFilters() { final query = controllers['tableSearch']?.text.trim().toLowerCase() ?? ''; setState(() { currentPage = 1; filteredPolicies = masterPolicies .where((item) => _matchesTableSearch(item, query)) .toList(); }); } void refresh() { setState(() { for (var controller in controllers.values) { controller.clear(); } selectedBrokerID = ''; selectedAgentId = []; selectedPolicies = {}; totalPolicies = ''; totalCommission = ''; masterPolicies = []; filteredPolicies = []; currentPage = 1; _disposeCommissionControllers(); hasFetchedTableData = false; // dropDownKeyPartner.currentState?.clear(); }); } // Get Broker List Future getBroker() async { print('PD =>getBroker called'); setState(() { isLoading = true; }); try { final response = await apiService.fetchMasterDropDown('Broker'); if (response['status'] == 200) { print('PD =>getBroker - ${response['data']}'); setState(() { getBrokerData = List>.from(response['data']); print('PD =>API Data - $getBrokerData'); filteredBrokerData = List.from(getBrokerData); print('PD =>originalData - $filteredBrokerData'); }); } else { getBrokerData = []; filteredBrokerData = []; } } catch (e) { print('PD =>Exception occurred: $e'); } finally { setState(() { isLoading = false; }); } } //Get Agent List Future getAgentList(int id) async { print('PD =>getClaimList called'); setState(() { isLoading = true; }); try { final response = await apiService.fetchAgentUserList(managerId); if (response['status'] == 'success') { print('PD =>getAgentListData - ${response['data']}'); setState(() { getAgentData = List>.from(response['data']); print('PD =>API Data - $getAgentData'); filteredAgentData = List.from(getAgentData); print('PD =>originalData - $filteredAgentData'); }); } else { getAgentData = []; filteredAgentData = []; } } catch (e) { print('PD =>Exception occurred: $e'); } finally { setState(() { isLoading = false; }); } } // Generate invoice number String generateInvoiceNumber() { final now = DateTime.now(); final random = now.millisecondsSinceEpoch % 1000; return "INV${now.year}${now.month.toString().padLeft(2, '0')}${random.toString().padLeft(3, '0')}"; } // Pick date helper Future pickDate(DateTime initial) async { return await showDatePicker( context: context, initialDate: initial, firstDate: DateTime(2000), lastDate: DateTime(2100), ); } // if (dashboardKey == 'f' && // isDashboardInitialLoad && // SelectedStatus == "P") { // // // fromDt = "";toDt = ""; // isDashboardInitialLoad = false; // // } else if (dashboardKey == 'f' && isDashboardInitialLoad && SelectedStatus != '') { // fromDt = "";toDt = ""; // isDashboardInitialLoad = false; // } else if (fromDateVal.isEmpty && toDateVal.isEmpty) { // // Default fallback // final today = DateTime.now(); // fromDt = DateFormat('dd-MM-yyyy').format(today.subtract(const Duration(days: 5))); // toDt = DateFormat('dd-MM-yyyy').format(today); // } else { // // ✅ USER SELECTED DATE — ALWAYS RESPECT THIS // fromDt = fromDateVal; // toDt = toDateVal; // } // Load policies (simulate API) Future loadPolicies() async { print('PD =>loadPolicies'); print("H13 => r : $roleId | mId: $managerId | uId: $userId "); setState(() { isLoading = true; hasFetchedTableData = false; }); try { final List agentIds = (selectedAgentId ?? []) .map((e) => e.toString().trim()) .where((e) => e.isNotEmpty) .toList(); final jsondata = { "from_date": controllers['startDate']?.text, "to_date": controllers['endDate']?.text, "manager_id": managerId, if (agentIds.isNotEmpty) "agent_id": agentIds, // "agent_id": agentIds, // "issued_date": formattedTillDate, }; final response = await apiService.getCommissionRateList(jsondata); if (response['status'] == 'success') { print('PD =>API Data response - $response'); setState(() { masterPolicies = List>.from(response['data']); filteredPolicies = List.from(masterPolicies); _rebuildCommissionControllers(masterPolicies); hasFetchedTableData = true; currentPage = 1; print('PD =>API Data - $filteredPolicies'); }); applyTableFilters(); } else { setState(() { masterPolicies = []; filteredPolicies = []; _rebuildCommissionControllers(filteredPolicies); hasFetchedTableData = true; currentPage = 1; }); } } catch (e) { print('PD =>Exception occurred: $e'); setState(() { masterPolicies = []; filteredPolicies = []; _rebuildCommissionControllers(filteredPolicies); hasFetchedTableData = false; currentPage = 1; }); } finally { setState(() { isLoading = false; }); } } // Apply date filters void filterPolicies() { if (selectedAgentId == null) return; setState(() { filteredPolicies = allPolicies.where((p) { final date = DateTime.parse(p["date"]); // if (p["agentId"] != selectedAgentId) return false; // if (policyTillDate != null && date.isAfter(policyTillDate!)) // return false; if (fromDate != null && date.isBefore(fromDate!)) return false; if (toDate != null && date.isAfter(toDate!)) return false; return true; }).toList(); }); } // Save invoice (simulation) Future saveInvoice() async { try { // if (selectedAgentId == null) { // ToastHelper.showWarningToast( // context, // "Please select a partner before saving.", // ); // return; // } if (selectedPolicies.isEmpty) { ToastHelper.showWarningToast( context, "Please select at least one policy before saving.", ); return; } final utrFormState = _utrFormKey.currentState; if (utrFormState != null && !utrFormState.validate()) { return; } final String utrInput = (controllers['utrNumber']?.text ?? '').trim(); if (utrInput.isEmpty) { ToastHelper.showWarningToast( context, "Please enter the UTR number. It cannot be left empty.", ); return; } if (!_isValidUtr(utrInput)) { ToastHelper.showWarningToast( context, "UTR must be $_utrMinLength–$_utrMaxLength letters or numbers only, " "with no spaces or special characters.", ); return; } // Format dates for API final String formattedInvoiceDate = DateFormat( 'yyyy-MM-dd', ).format(invoiceDate); final String formattedTillDate = DateFormat( 'yyyy-MM-dd', ).format(policyTillDate!); final List> selectedRows = filteredPolicies .where((p) { final id = int.tryParse(p["policy_id"]?.toString() ?? "0") ?? 0; return id != 0 && selectedPolicies.contains(id); }) .toList(); if (selectedRows.isEmpty) { ToastHelper.showWarningToast( context, "No matching policies found for your selection. Refresh the list and try again.", ); return; } for (final p in selectedRows) { final policyNo = (p['policy_no'] ?? '-').toString(); final rawPayout = (p['commission_amount'] ?? '').toString().trim(); if (rawPayout.isEmpty) { ToastHelper.showWarningToast( context, "Payout amount is missing for policy $policyNo. Enter an amount for every selected policy.", ); return; } final payout = double.tryParse(rawPayout); if (payout == null) { ToastHelper.showWarningToast( context, "Payout amount for policy $policyNo is not a valid number. Use digits only (e.g. 1500 or 1500.50).", ); return; } final premium = _parsePremiumAmount(p['premium_amount']); if (premium != null && payout > premium + 0.001) { ToastHelper.showWarningToast( context, "Payout for policy $policyNo (₹${payout.toStringAsFixed(2)}) cannot be greater than the premium (₹${premium.toStringAsFixed(2)}).", ); return; } } final double totalPayout = selectedRows.fold( 0.0, (sum, p) => sum + _parseCommissionValue(p["commission_amount"]), ); // Build items array final List> items = selectedRows .map( (p) => { "policy_id": int.parse(p["policy_id"].toString()), "policy_no": p["policy_no"], "commission_amount": _parseCommissionValue(p["commission_amount"]), }, ) .toList(); final List> utrs = _buildUtrPayloadForSelectedPolicies( defaultUtrDate: formattedInvoiceDate, ); if (utrs.isEmpty) { ToastHelper.showWarningToast( context, "UTR details could not be built. Check the UTR number and selected policies.", ); return; } final double totalUtrAmount = _sumUtrAmounts(utrs); if (!_amountsMatch(totalPayout, totalUtrAmount)) { ToastHelper.showWarningToast( context, "Total payout (₹${totalPayout.toStringAsFixed(2)}) must match the total UTR amount (₹${totalUtrAmount.toStringAsFixed(2)}). " "Check each row's payout and the UTR field.", ); return; } final int payoutStatus = _amountsMatch(totalPayout, totalUtrAmount) ? 2 : 1; final jsondata = { "invoice_no": controllers['invoiceNo']?.text, "invoice_amount": totalPayout, "agent_id": selectedAgentId, "broker_id": selectedBrokerID, "invoice_date": formattedInvoiceDate, "till_date": formattedTillDate, "payout_status": payoutStatus, "pos_id": null, "created_by": userId, "updated_by": userId, "items": items, "utrs": utrs, }; print('PD =>Commissions $jsondata'); final bool confirm = await showSaveConfirmation(context); if (!confirm) return; final response = await apiService.getCreateOrUpdate(jsondata); if (response['status'] == 'success') { setState(() { print('PD =>API Data response - $response'); selectedPolicies.clear(); controllers['invoiceNo']?.clear(); controllers['utrNumber']?.clear(); invoiceDate = DateTime.now(); policyTillDate = DateTime.now(); filteredPolicies = []; }); context.go(AppRoutes.payoutList); } else { final msg = (response['message'] ?? response['data'] ?? 'Save failed') .toString(); ToastHelper.showErrorToast(context, msg); } } catch (e) { print('PD =>Exception occurred: $e'); } finally { setState(() { isLoading = false; }); } } List> _buildUtrPayloadForSelectedPolicies({ required String defaultUtrDate, }) { final Map utrAmountMap = {}; final String enteredUtr = (controllers['utrNumber']?.text ?? '').trim(); for (final policy in filteredPolicies) { final int policyId = int.tryParse(policy["policy_id"]?.toString() ?? "0") ?? 0; if (!selectedPolicies.contains(policyId)) continue; final String utrNo = enteredUtr.isNotEmpty ? enteredUtr : (policy['utr_no'] ?? '').toString().trim(); if (utrNo.isEmpty) continue; final double amount = _parseCommissionValue(policy["commission_amount"]); utrAmountMap[utrNo] = (utrAmountMap[utrNo] ?? 0) + amount; } return utrAmountMap.entries .map( (e) => { "utr_no": e.key, "amount": e.value, "utr_date": defaultUtrDate, }, ) .toList(); } void showMessage(String msg) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); } Future showSaveConfirmation(BuildContext context) async { return await showDialog( context: context, barrierDismissible: false, builder: (context) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), title: Text( "Confirm Save", style: GoogleFonts.inter( fontSize: 16, fontWeight: FontWeight.w600, ), ), content: Text( "Once saved, payout details cannot be edited.\nAre you sure you want to continue?", style: GoogleFonts.inter(fontSize: 13, color: Colors.grey[700]), ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: Text( "No", style: GoogleFonts.inter( fontWeight: FontWeight.w500, color: Colors.grey[700], ), ), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF059669), // emerald shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), onPressed: () => Navigator.pop(context, true), child: Text( "Yes, Save", style: GoogleFonts.inter( fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ], ); }, ) ?? false; } @override Widget build(BuildContext context) { return MainLayout( title: "Pay Out", body: Container( color: Colors.white, padding: const EdgeInsets.all(8), child: Column( children: [ // const SizedBox(height: 20), Container( height: 40, width: MediaQuery.of(context).size.width, child: GestureDetector( onTap: null, child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.start, children: [ Tooltip( message: 'Back', child: IconButton( icon: const Icon( Icons.arrow_left_sharp, size: 25, color: Color(0xFF425B5B), ), onPressed: () { context.go(AppRoutes.payoutList); }, splashRadius: 18, hoverColor: Colors.black12, padding: const EdgeInsets.all(4), constraints: const BoxConstraints(), ), ), const SizedBox( width: 5, ), // spacing between icon and text Text( "Payout Details", style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w500, ), ), const Spacer(), if (hasFetchedTableData) ...[ const SizedBox(width: 10), Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6, ), decoration: BoxDecoration( color: const Color(0xFFECFDF5), // emerald bg borderRadius: BorderRadius.circular(20), ), child: Row( children: [ Text( "SELECTED POLICIES : ", style: GoogleFonts.poppins( fontSize: 10, fontWeight: FontWeight.w500, color: const Color(0xFF065F46), letterSpacing: 0.4, ), ), const SizedBox(width: 6), Text( totalPolicies, style: GoogleFonts.poppins( fontSize: 13, fontWeight: FontWeight.w700, color: const Color(0xFF047857), ), ), ], ), ), const SizedBox(width: 10), // 💰 TOTAL COMMISSION Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: const Color(0xFF10B981), // emerald green borderRadius: BorderRadius.circular(22), boxShadow: [ BoxShadow( color: const Color( 0xFF10B981, ).withOpacity(0.25), blurRadius: 6, offset: const Offset(0, 2), ), ], ), child: Row( children: [ const Icon( Icons.account_balance_wallet_outlined, size: 14, color: Colors.white, ), const SizedBox(width: 6), Text( "TOTAL PAYOUT AMOUNT :", style: GoogleFonts.poppins( fontSize: 10, fontWeight: FontWeight.w500, color: Colors.white.withOpacity(0.9), letterSpacing: 0.4, ), ), const SizedBox(width: 6), Text( "₹$totalCommission", style: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.w700, color: Colors.white, ), ), ], ), ), const SizedBox(width: 10), ExportBtn( sheetName: 'Payout Details', fileName: 'payout_details_${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}', data: _exportRows, displayHeaders: const [ 'S.NO', 'POLICY NO', 'POLICY DATE', 'PARTNER', 'CUSTOMER', 'PREMIUM (₹)', 'PAYOUT (₹)', ], keys: const [ 'sno', 'policy_no', 'policy_date', 'partner', 'customer', 'premium', 'payout', ], ), ], ], ), ), ), // SizedBox(height: 5), Container( padding: EdgeInsets.all(8.0), child: DateFilterRowPayout( formKey: _formKey, // key: ValueKey(SelectedStatus ?? ''), role: roleId, id: userId, showBrokerFilter: false, passDateRangeToAgentList: true, policyTableHasRows: filteredPolicies.isNotEmpty, selectedParnter: selectedAgentId, selectedBroker: selectedBrokerID, tableSearchFields: hasFetchedTableData ? _buildPolicyTableSearchFields(context) : null, onPartnerChanges: (val) { setState(() { selectedAgentId = val; // ✅ parent state updated }); }, selectedStaffId: '', startController: controllers['startDate']!, endController: controllers['endDate']!, isMobile: ResponsiveLayout.isMobile(context), onFilter: () { print( 'DATEROw- ${controllers['startDate']!} -${controllers['endDate']!}' ' - $selectedAgentId', ); // call your filter logic // filterDateRange(); loadPolicies(); }, onRefresh: () { // call your refresh logic refresh(); }, ), ), SizedBox(height: 10), // Policy Selection Card( // color: Colors.amber, color: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 0, child: Column( children: [ AnimatedCrossFade( duration: const Duration(milliseconds: 300), crossFadeState: policyCollapsed ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: const SizedBox(), secondChild: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _policyTableHeader(), _buildDataTable(context), ], ), ), ], ), ), if (selectedPolicies.isNotEmpty) ...[ Container( padding: const EdgeInsets.all(8), // color: Colors.amber, color: Color(0xFFf9fafb), // color: Colors.white, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ---------------- SELECTED COUNT ---------------- // Row( // crossAxisAlignment: CrossAxisAlignment.start, // children: [ // Text( // // selectedPolicies.length.toString(), // totalPolicies, // style: GoogleFonts.poppins( // fontSize: 12, // fontWeight: FontWeight.w500, // color: Colors.blueGrey, // ), // ), // const SizedBox(width: 4), // Text( // "policies selected", // style: GoogleFonts.poppins( // fontSize: 12, // fontWeight: FontWeight.w500, // color: Colors.blueGrey, // ), // ), // const SizedBox(width: 4), // Text( // "|", // style: GoogleFonts.poppins( // fontSize: 12, // fontWeight: FontWeight.w500, // color: Colors.blueGrey, // ), // ), // const SizedBox(width: 4), // Text( // "Total Payout Amount", // style: GoogleFonts.poppins( // fontSize: 12, // fontWeight: FontWeight.w500, // color: Colors.blueGrey, // ), // ), // const SizedBox(width: 4), // // /// NULL-SAFE TOTAL COMMISSION CALCULATION // Text( // // "₹ ${_calculateTotalCommission()}", // "₹ $totalCommission", // style: GoogleFonts.poppins( // fontSize: 14, // fontWeight: FontWeight.w500, // color: Colors.teal, // ), // ), // ], // ), Spacer(), buildUTRNumber(context), SizedBox(width: 20), // ---------------- BUTTONS ---------------- Padding( padding: const EdgeInsets.only(top: 2), child: Row( children: [ ElevatedButton( onPressed: saveInvoice, style: ElevatedButton.styleFrom( // backgroundColor: const Color(0xff45a049), backgroundColor: Colors.teal, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( 4, ), // 🔽 reduced radius ), padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 10, ), ), child: Text( "Save Payout Details", style: GoogleFonts.poppins( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500, ), ), ), const SizedBox(width: 10), TextButton( onPressed: () => setState(() => selectedPolicies.clear()), child: const Text("Clear"), ), ], ), ), ], ), ), ], if (filteredPolicies.isNotEmpty) Padding( padding: const EdgeInsets.fromLTRB(8, 4, 8, 2), child: Align( alignment: Alignment.centerRight, child: PaginationControls( currentPage: currentPage, itemsPerPage: itemsPerPage, totalItems: filteredPolicies.length, onPageChanged: (page) { setState(() => currentPage = page); }, onItemsPerPageChanged: (items) { setState(() { itemsPerPage = items; currentPage = 1; }); }, ), ), ), // ---------------------- // const SizedBox(height: 100), ], ), ), ); } Widget _policyTableHeader() { final allSelected = filteredPolicies.isNotEmpty && filteredPolicies.every( (p) => selectedPolicies.contains( int.tryParse(p["policy_id"]?.toString() ?? "0") ?? 0, ), ); // filteredPolicies.every( // (p) => selectedPolicies.contains( // int.tryParse(p["policy_id"].toString()) ?? 0, // ), // ); final partiallySelected = selectedPolicies.isNotEmpty && !allSelected; return Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), decoration: BoxDecoration( color: const Color(0xFFF1F5F9), borderRadius: BorderRadius.circular(6), ), child: Row( children: [ SizedBox( width: _colCheck, child: Center( child: Checkbox( value: allSelected ? true : (partiallySelected ? null : false), tristate: true, onChanged: (v) { setState(() { if (v == true) { selectedPolicies = filteredPolicies .map( (p) => int.tryParse( p["policy_id"]?.toString() ?? "0", ) ?? 0, ) .where((id) => id != 0) // Filter out invalid IDs .toSet(); } else { selectedPolicies.clear(); } totalPolicies = selectedPolicies.length.toString(); }); _calculateTotalCommission(); }, ), ), ), _headerText("S.NO", _flexSno, align: TextAlign.left), _headerText("POLICY NO", _flexPolicy, align: TextAlign.left), _headerText("POLICY DATE", _flexDate, align: TextAlign.left), _headerText("PARTNER", _flexAgent, align: TextAlign.left), _headerText("CUSTOMER", _flexCustomer, align: TextAlign.left), _headerText("PREMIUM (₹)", _flexPremium, align: TextAlign.right), SizedBox(width: 10), _headerText("PAYOUT (₹)", _flexCommission, align: TextAlign.right), ], ), ); } Widget _headerText(String text, int flex, {TextAlign align = TextAlign.left}) { return Expanded( flex: flex, child: Text( text, textAlign: align, style: _tableHeaderStyle, ), ); } Widget _buildPolicyTableSearchFields(BuildContext context) { final width = ResponsiveLayout.isMobile(context) ? MediaQuery.of(context).size.width * 0.55 : MediaQuery.of(context).size.width * 0.2; return SizedBox( width: width, child: ThemedSearchField( hintText: 'Search', backgroundColor: Colors.white, txtHeight: 30, controller: controllers['tableSearch']!, txtwidth: width, onChanged: (_) => applyTableFilters(), ), ); } Widget _buildDataTable(BuildContext context) { if (filteredPolicies.isEmpty) { return const SizedBox( height: 50, child: Center(child: Text('No available data')), ); } final pageRows = _paginatedPolicies; return SizedBox( height: MediaQuery.of(context).size.height * 0.48, child: ListView.builder( itemCount: pageRows.length + 1, itemBuilder: (context, index) { if (index == 0) return _buildHeader(); final startIndex = (currentPage - 1) * itemsPerPage; final item = pageRows[index - 1]; final sno = startIndex + index; return _buildDataRow(item, sno); }, ), ); } Widget _buildHeader() { return const SizedBox.shrink(); } Widget _buildDataRow(Map item, int sno) { final int id = int.tryParse(item["policy_id"]?.toString() ?? "0") ?? 0; final bool isSelected = selectedPolicies.contains(id); final partner = _partnerDisplayText(item); return InkWell( onTap: () { setState(() { isSelected ? selectedPolicies.remove(id) : selectedPolicies.add(id); totalPolicies = selectedPolicies.length.toString(); }); _calculateTotalCommission(); }, child: Container( padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16), decoration: BoxDecoration( color: isSelected ? const Color(0xFFF0FAF6) : Colors.white, border: const Border( bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1), ), borderRadius: BorderRadius.circular(8), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( width: _colCheck, child: Center( child: Checkbox( value: isSelected, onChanged: (v) { setState(() { v == true ? selectedPolicies.add(id) : selectedPolicies.remove(id); totalPolicies = selectedPolicies.length.toString(); }); _calculateTotalCommission(); }, ), ), ), Expanded( flex: _flexSno, child: Text( '$sno', textAlign: TextAlign.left, style: _tableDataStyle, overflow: TextOverflow.ellipsis, maxLines: 2, ), ), Expanded( flex: _flexPolicy, child: Text( (item["policy_no"] ?? "-").toString(), textAlign: TextAlign.left, style: _tableDataStyle, overflow: TextOverflow.ellipsis, maxLines: 2, ), ), Expanded( flex: _flexDate, child: Text( _formatDate(item['issued_date']), textAlign: TextAlign.left, style: _tableDataStyle, overflow: TextOverflow.ellipsis, maxLines: 1, ), ), Expanded( flex: _flexAgent, child: Text( partner.isEmpty ? '-' : partner, textAlign: TextAlign.left, style: _tableDataStyle, overflow: TextOverflow.ellipsis, maxLines: 2, ), ), Expanded( flex: _flexCustomer, child: Text( (item["customer_name"] ?? "-").toString(), textAlign: TextAlign.left, style: _tableDataStyle, overflow: TextOverflow.ellipsis, maxLines: 2, ), ), Expanded( flex: _flexPremium, child: Text( _amountText(item["premium_amount"]), textAlign: TextAlign.right, style: _tableDataStyle, overflow: TextOverflow.ellipsis, maxLines: 1, ), ), SizedBox(width: 10), Expanded( flex: _flexCommission, child: Align( alignment: Alignment.centerRight, child: ConstrainedBox( constraints: const BoxConstraints( minWidth: 76, maxWidth: 112, ), child: _commissionAmountField( item, _commissionControllerFor(item), ), ), ), ), ], ), ), ); } String _amountText(dynamic value) { final raw = (value ?? '').toString().replaceAll('₹', '').trim(); if (raw.isEmpty || raw.toLowerCase() == 'null') return '-'; return raw; } List> get _paginatedPolicies { if (filteredPolicies.isEmpty) return const >[]; final start = (currentPage - 1) * itemsPerPage; final end = (start + itemsPerPage).clamp(0, filteredPolicies.length); return filteredPolicies.sublist(start, end); } List> get _exportRows { return filteredPolicies.map((p) { return { 'sno': 0, 'policy_no': p['policy_no'], 'policy_date': _formatDate((p['issued_date'] ?? '').toString()), 'partner': _partnerDisplayText(p).isEmpty ? '-' : _partnerDisplayText(p), 'customer': p['customer_name'], 'premium': _amountText(p['premium_amount']), 'payout': _amountText(p['commission_amount']), }; }).toList(); } double _parseCommissionValue(dynamic value) { if (value == null) return 0; return double.tryParse(value.toString().trim()) ?? 0; } /// Premium from API / table (may include ₹ or commas). double? _parsePremiumAmount(dynamic value) { if (value == null) return null; final cleaned = value .toString() .replaceAll('₹', '') .replaceAll(',', '') .replaceAll(RegExp(r'\s+'), '') .trim(); if (cleaned.isEmpty) return null; return double.tryParse(cleaned); } double _sumUtrAmounts(List> utrs) { double sum = 0; for (final row in utrs) { sum += _parseCommissionValue(row['amount']); } return sum; } bool _amountsMatch(double a, double b, {double epsilon = 0.01}) { return (a - b).abs() < epsilon; } Future _updateInlineCommission(Map row, String value) async { final trimmed = value.trim(); final amount = double.tryParse(trimmed); if (amount == null) { ToastHelper.showWarningToast(context, 'Invalid payout amount'); return; } final int id = int.tryParse((row['id'] ?? row['policy_id'] ?? '').toString()) ?? 0; if (id == 0 || _inlineSavingIds.contains(id)) return; setState(() { _inlineSavingIds.add(id); }); try { final response = await apiService.createUserData( { 'id': id, 'commission_amount': amount, }, 'policy/payoutInilneEditUpdate', ); if (response['status'] == 'success' || response['status'] == 200) { row['commission_amount'] = trimmed; final cid = _policyRowId(row); final ctrl = _commissionControllers[cid]; if (ctrl != null && ctrl.text != trimmed) { ctrl.value = TextEditingValue( text: trimmed, selection: TextSelection.collapsed(offset: trimmed.length), ); } _calculateTotalCommission(); } else { ToastHelper.showErrorToast( context, (response['message'] ?? 'Inline update failed').toString(), ); } } catch (e) { ToastHelper.showErrorToast(context, e.toString()); } finally { if (mounted) { setState(() { _inlineSavingIds.remove(id); }); } } } double _calculateTotalCommission() { print("ASD"); print(filteredPolicies); double total = 0.0; for (var p in filteredPolicies) { // Safe policy_id parsing final String idStr = p["policy_id"]?.toString() ?? "0"; final int id = int.tryParse(idStr) ?? 0; if (!selectedPolicies.contains(id)) continue; total += _parseCommissionValue(p["commission_amount"]); } setState(() { totalCommission = total.roundToDouble(); }); // Remove decimal points (.00) return total.roundToDouble(); } Widget buildUTRNumber(BuildContext context) { final fieldWidth = MediaQuery.of(context).size.width * 0.15; final utrDecoration = commonInputDecoration( hint: 'UTR ($_utrMinLength–$_utrMaxLength chars)', ).copyWith( hintStyle: GoogleFonts.poppins( fontSize: 9, fontWeight: FontWeight.w400, color: const Color(0xFF9CA3AF), ), isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), errorStyle: GoogleFonts.poppins( fontSize: 10, fontWeight: FontWeight.w500, color: const Color(0xFFB91C1C), height: 1.25, ), errorMaxLines: 3, ); return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(top: 10), child: Text('UTR Number', style: _textStyle), ), const SizedBox(width: 5), SizedBox( width: fieldWidth.clamp(160.0, 280.0), child: Form( key: _utrFormKey, child: TextFormField( controller: controllers['utrNumber']!, keyboardType: TextInputType.text, autocorrect: false, inputFormatters: _utrInputFormatter, autovalidateMode: AutovalidateMode.onUserInteraction, validator: (value) { final v = (value ?? '').trim(); if (v.isEmpty) return null; if (!_isValidUtr(v)) { return 'Use $_utrMinLength–$_utrMaxLength letters or numbers only. ' 'No spaces or symbols.'; } return null; }, decoration: utrDecoration, ), ), ), ], ); } InputDecoration commonInputDecoration({required String hint}) { return InputDecoration( hintText: hint, filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric(horizontal: 12), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: const BorderSide( color: Color(0xFFDDDDDD), // light gray width: 1, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: const BorderSide( color: Color(0xFFAAAAAA), // darker gray on focus width: 1.2, ), ), ); } static final _textStyle = GoogleFonts.poppins( fontSize: 11, fontWeight: FontWeight.w500, ); static final _tableDataStyle = GoogleFonts.inter( fontSize: 12, height: 1.4, fontWeight: FontWeight.w400, color: const Color(0xFF000000), ); static final _tableHeaderStyle = GoogleFonts.poppins( fontSize: 11.2, fontWeight: FontWeight.w500, color: const Color(0xFF1E293B), ); String _formatDate(String rawDate) { try { final dateTime = DateTime.parse(rawDate); return DateFormat('dd-MM-yyyy').format(dateTime); // 24-hour format } catch (e) { return rawDate; // fallback if parsing fails } } static const double _colCheck = 44; /// Wider columns get more flex; narrow ids/dates get less to avoid empty middle gaps. static const int _flexSno = 1; static const int _flexPolicy = 2; static const int _flexDate = 1; static const int _flexAgent = 3; static const int _flexCustomer = 3; static const int _flexPremium = 1; static const int _flexCommission = 1; }