import 'dart:convert'; import 'dart:typed_data'; import 'package:dropdown_search/dropdown_search.dart'; import 'package:excel/excel.dart' hide Border; import 'package:file_picker/file_picker.dart'; 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 '../../../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/input_field_decoration.dart'; import '../../themes/indicators/search_field_theme.dart'; import 'FormFieldBox.dart'; import 'custom_dateRange.dart'; class PayOutScreen extends ConsumerStatefulWidget { final Map? editItem; const PayOutScreen({super.key, this.editItem}); @override ConsumerState createState() => _PayOutScreenState(); } class _PayOutScreenState extends ConsumerState { static final List _commissionInputFormatter = [ FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')), ]; // -------------------------- // Invoice Fields // -------------------------- final _formKey = GlobalKey(); // final TextEditingController invoiceNoController; DateTime invoiceDate = DateTime.now(); // dynamic selectedAgentId; List? selectedAgentId; dynamic selectedBrokerID; dynamic selectedPOS; DateTime? policyTillDate = DateTime.now(); dynamic totalPolicies = '0'; dynamic totalCommission = '0'; bool isEdit = false; List> masterPolicies = []; final TextEditingController _searchStaffController = TextEditingController(); final GlobalKey>> dropDownKeyPOS = GlobalKey>>(); // -------------------------- // Filters // -------------------------- DateTime? fromDate; DateTime? toDate; Map controllers = {}; List tabHeader = [ 'startDate', 'endDate', 'invoiceNo', 'invoiceDate', 'invoiceStatus', ]; // -------------------------- // 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 = []; List> getPOSDataList = []; List> originalPOSData = []; List> filteredPOSData = []; bool isLoading = false; bool isLoadingPOS = false; bool isLoadingEditData = false; bool hasFetchedTableData = false; final Map _uploadedUtrByPolicyKey = {}; 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('PayOutScreen => managerId: $managerId'); if (managerId != null) { getAgentList(managerId); getPosList(managerId); } await getBroker(); // ✅ EDIT MODE (MUST be here) if (widget.editItem != null) { print('editItemID ${widget.editItem}'); final invoiceID = widget.editItem!['id']; final brokerID = widget.editItem!['broker_id']; controllers['invoiceNo']?.text = widget.editItem!['invoice_no']; controllers['invoiceDate']?.text = widget.editItem!['invoice_date_ui_format']; controllers['invoiceStatus']?.text = widget.editItem!['payout_status']; _loadEditData(invoiceID, brokerID, managerId); // ✅ managerId ready } }); } void filterPolicyData(String query) { final lowerQuery = query.toLowerCase(); print('filterPolicyData - $lowerQuery'); print('filteredPolicies1 - $filteredPolicies'); setState(() { if (query.trim().isEmpty) { print('filteredPolicies2'); filteredPolicies = List.from(masterPolicies); return; } print('filteredPolicies3'); filteredPolicies = masterPolicies.where((item) { return (item['policy_no'] ?? '').toString().toLowerCase().contains( lowerQuery, ) || (item['customer_name'] ?? '').toString().toLowerCase().contains( lowerQuery, ) || (item['agent_name'] ?? '').toString().toLowerCase().contains( lowerQuery, ) || (item['insurer_name'] ?? '').toString().toLowerCase().contains( lowerQuery, ) || (item['premium_amount'] ?? '').toString().toLowerCase().contains( lowerQuery, ) || (item['commission_amount'] ?? '').toString().toLowerCase().contains( lowerQuery, ) || (_formatDate(item['issued_date']) ?? '').toLowerCase().contains( lowerQuery, ); }).toList(); }); } Future getPosList(int id) async { print('H104 => Fns called => $id'); final val = 'dropDown'; setState(() { isLoadingPOS = true; }); try { final response = await apiService.fetchPosList(id, val); if (response['status'] == 200) { print('E113 => getStaffListData => ${response['data']}'); setState(() { getPOSDataList = List>.from(response['data']); originalPOSData = getPOSDataList; filteredPOSData = List.from(originalPOSData); // print('originalData - $getClaimPolicies'); }); } else { getPOSDataList = []; originalPOSData = []; } } catch (e) { print('Exception occurred: $e'); } finally { setState(() { isLoadingPOS = false; }); } } void refresh() { setState(() { for (var controller in controllers.values) { controller.clear(); controller.dispose(); } selectedPOS = ''; selectedBrokerID = ''; selectedAgentId = []; selectedPolicies = {}; totalPolicies = ''; totalCommission = ''; filteredPolicies = []; hasFetchedTableData = false; // dropDownKeyPartner.currentState?.clear(); }); } void _loadEditData(String invoiceID, brokerID, managerID) async { setState(() => isLoadingEditData = true); print("H010 => r : $roleId | mId: $managerID | ParmMID: $managerID | uId: $userId "); print('_loadEditData'); final jsondata = { // "id": invoiceID, "invoice_id": invoiceID, "broker_id": int.tryParse(brokerID ?? ''), "manager_id": managerId // "issued_date": formattedTillDate, }; print('_loadEditData - $jsondata'); final response = await apiService.getCommissionRateList(jsondata); // final response = await apiService.getInvoiceDetails(invoiceID); if (response['status'] == 'success') { print('_loadEditData success'); print('_loadEditData success - ${response['data']}'); final invoice = response['data']; print("Invoice → $invoice"); // final items = List>.from(response['data']['items']); final items = response['data']; final totPolicy = response['total_policies'].toString() ?? "0"; final totalCommiss = response['total_commission']; setState(() { // selectedPolicies = response['total_policies']; isEdit = true; totalPolicies = totPolicy; totalCommission = totalCommiss; // filteredPolicies = items.map((p) { // final policyId = int.parse(p['policy_id']); // return {...p, 'is_selected': selectedPolicies.contains(policyId)}; // }).toList(); final List> items = (response['data'] as List) .map((e) => Map.from(e)) .toList(); masterPolicies = List>.from(response['data']); filteredPolicies = List>.from(response['data']); print("selectedPolicies → $filteredPolicies "); }); print( "selectedPolicies → $selectedPolicies ->${response['total_policies']}", ); print("Items → $items"); // ----------------------------- // 1. SET BASIC FIELDS // ----------------------------- // setState(() { // // selectedAgentId = int.tryParse(invoice['agent_id'].toString()); // // // invoiceNoController.text = invoice['invoice_no']; // // invoiceDate = DateTime.parse(invoice['invoice_date']); // // controllers['startDate']?.text = invoice['from_date']; // controllers['endDate']?.text = invoice['to_date']; // selectedBrokerID = int.tryParse(invoice['broker_id'].toString()); // if (invoice['agent_id'] is List) { // selectedAgentId = (invoice['agent_id'] as List) // .map((e) => e.toString()) // .toList(); // } else if (invoice['agent_id'] is String) { // final raw = invoice['agent_id'].toString(); // // if (raw.startsWith('[')) { // // JSON string: "[1,2]" // selectedAgentId = List.from( // jsonDecode(raw).map((e) => e.toString()), // ); // } else if (raw.contains(',')) { // // Comma separated: "1,2" // selectedAgentId = raw.split(',').map((e) => e.trim()).toList(); // } else if (raw.isNotEmpty) { // // Single value: "1" // selectedAgentId = [raw]; // } else { // selectedAgentId = []; // } // } else { // selectedAgentId = []; // } // // // if (invoice['till_date'] != "0000-00-00") { // // policyTillDate = DateTime.parse(invoice['till_date']); // // } else { // // policyTillDate = DateTime.now(); // // } // }); // print('selectedBrokerID $selectedBrokerID'); // ----------------------------- // 3. LOAD ALL POLICIES FOR THIS BROKER + AGENT // ----------------------------- // await loadPolicies(); // ----------------------------- // 2. SET SELECTED POLICIES // ----------------------------- // selectedPolicies = items // .map((p) => int.parse(p['policy_id'].toString())) // .toSet(); // ----------------------------- // 3. LOAD ALL POLICIES FOR THIS BROKER + AGENT // ----------------------------- // await loadPolicies(); // ----------------------------- // 4. KEEP ONLY POLICIES THAT MATCH API DATA // ----------------------------- // setState(() { // filteredPolicies = filteredPolicies.map((p) { // final policyId = int.parse(p['policy_id']); // return {...p, 'is_selected': selectedPolicies.contains(policyId)}; // }).toList(); // }); } setState(() => isLoadingEditData = false); } String formatDate(String date) { final d = DateTime.parse(date); return DateFormat('dd-MM-yyyy').format(d); } // Get Broker List Future getBroker() async { print('getBroker called'); setState(() { isLoading = true; }); try { final response = await apiService.fetchMasterDropDown('Broker'); if (response['status'] == 200) { print('getBroker - ${response['data']}'); setState(() { getBrokerData = List>.from(response['data']); print('API Data - $getBrokerData'); filteredBrokerData = List.from(getBrokerData); print('originalData - $filteredBrokerData'); }); } else { getBrokerData = []; filteredBrokerData = []; } } catch (e) { print('Exception occurred: $e'); } finally { setState(() { isLoading = false; }); } } //Get Agent List Future getAgentList(int id) async { print('getClaimList called'); setState(() { isLoading = true; }); try { final response = await apiService.fetchAgentUserList(managerId); if (response['status'] == 'success') { print('getAgentListData - ${response['data']}'); setState(() { getAgentData = List>.from(response['data']); print('API Data - $getAgentData'); filteredAgentData = List.from(getAgentData); print('originalData - $filteredAgentData'); }); } else { getAgentData = []; filteredAgentData = []; } } catch (e) { print('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('loadPolicies'); print("H13 => r : $roleId | mId: $managerId | uId: $userId "); setState(() { isLoading = true; hasFetchedTableData = false; }); try { final List agentIds = (selectedAgentId ?? []) .map((e) => int.parse(e)) .toList(); final jsondata = { "from_date": controllers['startDate']?.text, "to_date": controllers['endDate']?.text, "broker_id": int.tryParse(selectedBrokerID ?? ''), "manager_id": managerId, if (agentIds != null && agentIds.isNotEmpty) "agent_id": agentIds, // "agent_id": agentIds, // "issued_date": formattedTillDate, }; final response = await apiService.getCommissionRateList(jsondata); if (response['status'] == 'success') { print('API Data response - $response'); setState(() { filteredPolicies = List>.from(response['data']); hasFetchedTableData = true; print('API Data - $filteredPolicies'); // filteredPolicies = allPolicies.where((p) { // if (p["agentId"] != selectedAgentId) return false; // if (policyTillDate != null && // DateTime.parse(p["date"]).isAfter(policyTillDate!)) // return false; // return true; // }).toList(); }); } else { setState(() { filteredPolicies = []; hasFetchedTableData = true; }); } } catch (e) { print('Exception occurred: $e'); setState(() { filteredPolicies = []; hasFetchedTableData = false; }); } 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 an agent"); return; } final bool isNhanceBroker = _isNhanceBrokerSelected(); final bool hasValidPosSelection = selectedPOS != null && selectedPOS.toString().trim().isNotEmpty; if (isNhanceBroker && !hasValidPosSelection) { ToastHelper.showWarningToast(context, "Please select POS"); return; } if (selectedPolicies.isEmpty) { ToastHelper.showWarningToast( context, "Please select at least 1 policy", ); return; } // Format dates for API final String formattedInvoiceDate = DateFormat( 'yyyy-MM-dd', ).format(invoiceDate); final String formattedTillDate = DateFormat( 'yyyy-MM-dd', ).format(policyTillDate!); // Total Commission final double totalCommission = filteredPolicies .where((p) => selectedPolicies.contains(int.parse(p["policy_id"]))) .fold(0.0, (sum, p) => sum + _parseCommissionValue(p["commission_amount"])); // Build items array final List> items = filteredPolicies .where((p) => selectedPolicies.contains(int.parse(p["policy_id"]))) .map( (p) => { "policy_id": int.parse(p["policy_id"]), "policy_no": p["policy_no"], "commission_amount": _parseCommissionValue(p["commission_amount"]), }, ) .toList(); final List> utrs = _buildUtrPayloadForSelectedPolicies( defaultUtrDate: formattedInvoiceDate, ); final jsondata = { "invoice_no": controllers['invoiceNo']?.text, "invoice_amount": totalCommission, "agent_id": selectedAgentId, "broker_id": selectedBrokerID, "invoice_date": formattedInvoiceDate, "till_date": formattedTillDate, "payout_status": 0, "pos_id": isNhanceBroker ? selectedPOS : 0, "created_by": userId, "updated_by": userId, "items": items, "utrs": utrs, }; print('Commissions $jsondata'); final bool confirm = await showSaveConfirmation(context); if (!confirm) return; final response = await apiService.getCreateOrUpdate(jsondata); if (response['status'] == 'success') { setState(() { print('API Data response - $response'); selectedPolicies.clear(); controllers['invoiceNo']?.clear(); invoiceDate = DateTime.now(); policyTillDate = DateTime.now(); filteredPolicies = []; _uploadedUtrByPolicyKey.clear(); }); context.go(AppRoutes.invoiceList); } else { filteredPolicies = []; } } catch (e) { print('Exception occurred: $e'); } finally { setState(() { isLoading = false; }); } } bool _isNhanceBrokerSelected() { final selectedBroker = getBrokerData.firstWhere( (item) => item['id'].toString() == (selectedBrokerID ?? '').toString(), orElse: () => {}, ); if (selectedBroker.isEmpty) return false; final brokerName = (selectedBroker['name'] ?? '').toString().toLowerCase(); return brokerName.contains('nhance'); } String _normalizeHeader(dynamic header) { return header .toString() .trim() .toLowerCase() .replaceAll(RegExp(r'[^a-z0-9]'), ''); } String _excelCellValue(Data? cell) { final value = cell?.value; if (value == null) return ''; return value.toString().trim(); } String _normalizePolicyNo(dynamic value) { return (value ?? '') .toString() .trim() .toLowerCase() .replaceAll(RegExp(r'\s+'), ''); } String _displayText(dynamic value) { if (value == null) return '-'; final text = value.toString().trim(); if (text.isEmpty || text.toLowerCase() == 'null') return '-'; return text; } double? _parseExcelCommission(String raw) { final cleaned = raw .replaceAll('₹', '') .replaceAll(',', '') .replaceAll(RegExp(r'\s+'), '') .trim(); if (cleaned.isEmpty) return null; return double.tryParse(cleaned); } List> _buildUtrPayloadForSelectedPolicies({ required String defaultUtrDate, }) { /* * Build UTR payload from selected policies. * 1) UTR source is Excel "UTR Number" column captured per policy. * 2) If same UTR appears in multiple selected rows, amount is aggregated. * 3) Amount comes from selected policy commission amount. */ final Map utrAmountMap = {}; for (final policy in filteredPolicies) { final int policyId = int.tryParse(policy["policy_id"]?.toString() ?? "0") ?? 0; if (!selectedPolicies.contains(policyId)) continue; final String policyKey = _normalizePolicyNo(policy['policy_no']); final String utrNo = (_uploadedUtrByPolicyKey[policyKey] ?? '').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(); } Widget _excelMatchedPolicyTile(Map row) { final givenUtr = (row['given_utr_no'] ?? '').trim(); return Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(10), decoration: BoxDecoration( border: Border.all(color: const Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Policy number: ${row['policy_no']}'), const SizedBox(height: 4), Text('Current commission: ${row['current_commission']}'), const SizedBox(height: 4), Text('Excel commission: ${row['excel_commission']}'), if (givenUtr.isNotEmpty) ...[ const SizedBox(height: 4), Text('UTR number: $givenUtr'), ], ], ), ); } /// No Excel row matched the table — list all Excel policy numbers; OK only. Future _showAllPoliciesUnmatchedDialog({ required List excelPolicyNumbersNotMatched, required int invalidExcelRows, }) async { await showDialog( context: context, barrierDismissible: false, builder: (ctx) => AlertDialog( title: Text( 'No policies matched', style: GoogleFonts.inter(fontWeight: FontWeight.w600), ), content: SizedBox( width: 520, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'None of the policy numbers in the Excel file match the policies in the current table.', style: GoogleFonts.inter(fontSize: 13, height: 1.35), ), if (excelPolicyNumbersNotMatched.isNotEmpty) ...[ const SizedBox(height: 12), Text( 'Policy numbers from Excel (not matched):', style: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), ...excelPolicyNumbersNotMatched.map( (p) => Padding( padding: const EdgeInsets.only(bottom: 4), child: Text('• $p', style: GoogleFonts.inter(fontSize: 13)), ), ), ], if (invalidExcelRows > 0) ...[ const SizedBox(height: 12), Text( '$invalidExcelRows row(s) in Excel were skipped (invalid policy number or commission).', style: GoogleFonts.inter(fontSize: 12, color: Colors.grey[700]), ), ], ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: Text('OK', style: GoogleFonts.inter(fontWeight: FontWeight.w600)), ), ], ), ); } /// At least one match — show matched one-by-one; if Excel has extra policies, list them; Proceed / Cancel. Future _showUploadReviewProceedDialog({ required List> matchedRows, required List excelPolicyNumbersNotOnTable, required int invalidExcelRows, }) async { final hasExcelOnly = excelPolicyNumbersNotOnTable.isNotEmpty; return await showDialog( context: context, barrierDismissible: false, builder: (ctx) => AlertDialog( title: Text( hasExcelOnly ? 'Review Excel vs table' : 'Confirm commission update', style: GoogleFonts.inter(fontWeight: FontWeight.w600), ), content: SizedBox( width: 560, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Matched policies (${matchedRows.length})', style: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), ...matchedRows.map(_excelMatchedPolicyTile), if (hasExcelOnly) ...[ const SizedBox(height: 16), Text( 'Not found on current table (${excelPolicyNumbersNotOnTable.length})', style: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w600, color: Colors.orange.shade800, ), ), const SizedBox(height: 6), Text( 'These policy numbers are in Excel but not in the loaded table:', style: GoogleFonts.inter(fontSize: 12, height: 1.35), ), const SizedBox(height: 8), ...excelPolicyNumbersNotOnTable.map( (p) => Padding( padding: const EdgeInsets.only(bottom: 4), child: Text( '• $p', style: GoogleFonts.inter(fontSize: 13), ), ), ), ], if (invalidExcelRows > 0) ...[ const SizedBox(height: 12), Text( 'Note: $invalidExcelRows Excel row(s) skipped (invalid data).', style: GoogleFonts.inter(fontSize: 12, color: Colors.grey[700]), ), ], ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: Text('Cancel', style: GoogleFonts.inter()), ), ElevatedButton( onPressed: () => Navigator.pop(ctx, true), child: Text('Proceed', style: GoogleFonts.inter()), ), ], ), ) ?? false; } Future _showMismatchProceedDialog(int mismatchCount) async { return await showDialog( context: context, barrierDismissible: false, builder: (ctx) => AlertDialog( title: const Text('Partner mismatch found'), content: Text( '$mismatchCount row(s) are linked to a different partner.\n' 'Proceeding will retroactively update partner mapping in payout records.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel'), ), ElevatedButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Proceed'), ), ], ), ) ?? false; } Future _triggerBulkUpload() async { final brokerId = int.tryParse( (selectedBrokerID ?? '').toString().trim(), ); if (brokerId == null) { ToastHelper.showWarningToast( context, 'Please select a broker before uploading.', ); return; } if (selectedAgentId == null || selectedAgentId!.isEmpty) { ToastHelper.showWarningToast( context, 'Please select Referrer before uploading.', ); return; } if (!hasFetchedTableData || filteredPolicies.isEmpty) { ToastHelper.showWarningToast( context, 'No policy data loaded. Set dates, Referrer, broker, tap Filter, then upload.', ); return; } try { final picked = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['xlsx', 'xls'], withData: true, ); if (picked == null || picked.files.isEmpty) { ToastHelper.showWarningToast( context, 'No Excel file selected. Please choose a file to upload.', ); return; } final selectedFile = picked.files.single; if (selectedFile.bytes == null || selectedFile.bytes!.isEmpty) { ToastHelper.showWarningToast( context, 'Unable to read the selected Excel file. Try another file.', ); return; } setState(() => isLoading = true); final excel = Excel.decodeBytes(Uint8List.fromList(selectedFile.bytes!)); if (excel.tables.isEmpty) { ToastHelper.showWarningToast( context, 'Invalid Excel: no sheet found in the file.', ); return; } final sheet = excel.tables.values.first; final rows = sheet.rows; if (rows.isEmpty) { ToastHelper.showWarningToast( context, 'Uploaded Excel file is empty.', ); return; } final headerRow = rows.first; int policyNoCol = -1; int commissionCol = -1; int utrNoCol = -1; for (int i = 0; i < headerRow.length; i++) { final normalized = _normalizeHeader(_excelCellValue(headerRow[i])); if (normalized == 'policynumber') policyNoCol = i; if (normalized == 'commissionamount') commissionCol = i; if (normalized == 'utrnumber') utrNoCol = i; } if (policyNoCol == -1 || commissionCol == -1) { ToastHelper.showWarningToast( context, 'Excel is missing required columns: Policy Number and Commission Amount.', ); return; } final Map uploadMap = {}; final Map uploadPolicyDisplay = {}; final Map uploadUtrMap = {}; final Set seenUtrs = {}; final Set duplicateUtrs = {}; int invalidRows = 0; for (int rowIndex = 1; rowIndex < rows.length; rowIndex++) { final row = rows[rowIndex]; final policyRaw = row.length > policyNoCol ? _excelCellValue(row[policyNoCol]) : ''; final commissionRaw = row.length > commissionCol ? _excelCellValue(row[commissionCol]) : ''; final utrRaw = utrNoCol >= 0 && row.length > utrNoCol ? _excelCellValue(row[utrNoCol]) : ''; // Ignore fully empty Excel rows; do not count as invalid. if (policyRaw.trim().isEmpty && commissionRaw.trim().isEmpty && utrRaw.trim().isEmpty) { continue; } final normalizedPolicy = _normalizePolicyNo(policyRaw); final parsedCommission = _parseExcelCommission(commissionRaw); if (normalizedPolicy.isEmpty || parsedCommission == null) { invalidRows++; continue; } uploadMap[normalizedPolicy] = parsedCommission; final display = policyRaw.trim().isEmpty ? normalizedPolicy : policyRaw.trim(); uploadPolicyDisplay[normalizedPolicy] = display; if (utrRaw.trim().isNotEmpty) { final cleanedUtr = utrRaw.trim(); // Enforce unique UTR numbers inside uploaded Excel file. if (seenUtrs.contains(cleanedUtr)) { duplicateUtrs.add(cleanedUtr); } else { seenUtrs.add(cleanedUtr); uploadUtrMap[normalizedPolicy] = cleanedUtr; } } } if (duplicateUtrs.isNotEmpty) { ToastHelper.showWarningToast( context, 'UTR number repeated in Excel: ${duplicateUtrs.join(", ")}', ); return; } if (uploadMap.isEmpty) { ToastHelper.showWarningToast( context, 'No valid data rows in Excel (check policy numbers and commission amounts).', ); return; } final Set screenPolicyKeys = { for (final p in filteredPolicies) _normalizePolicyNo(p['policy_no']), }; final List> matchedPolicyRefs = []; final List> matchedRowsForDialog = []; final List excelPoliciesNotOnTable = []; int utrComparedCount = 0; int utrChangedCount = 0; int utrSameCount = 0; for (final MapEntry e in uploadMap.entries) { final key = e.key; if (screenPolicyKeys.contains(key)) { final policy = filteredPolicies.firstWhere( (p) => _normalizePolicyNo(p['policy_no']) == key, ); final excelUtr = (uploadUtrMap[key] ?? '').trim(); final tableUtr = _displayText(policy['utr_no']); if (excelUtr.isNotEmpty) { utrComparedCount++; if (tableUtr != '-' && tableUtr == excelUtr) { utrSameCount++; } else { utrChangedCount++; } } matchedPolicyRefs.add(policy); matchedRowsForDialog.add({ 'policy_no': (policy['policy_no'] ?? '').toString(), 'current_commission': (policy['commission_amount'] ?? '').toString(), 'excel_commission': uploadMap[key]!.toStringAsFixed(2), 'given_utr_no': excelUtr, }); } else { excelPoliciesNotOnTable.add(uploadPolicyDisplay[key] ?? key); } } final int matched = matchedRowsForDialog.length; final int tableRowsNotInExcel = filteredPolicies .where((p) => !uploadMap.containsKey(_normalizePolicyNo(p['policy_no']))) .length; print( '[PayOut Excel comparison] referAgents=${selectedAgentId?.length ?? 0} ' 'brokerId=$brokerId | onScreenPolicies=${filteredPolicies.length} | ' 'excelValidPolicies=${uploadMap.length} | matchedForUpdate=$matched | ' 'excelNotOnTable=${excelPoliciesNotOnTable.length} | ' 'tableRowsNotInExcel=$tableRowsNotInExcel | excelRowsSkippedInvalid=$invalidRows', ); if (matched == 0) { await _showAllPoliciesUnmatchedDialog( excelPolicyNumbersNotMatched: excelPoliciesNotOnTable.isNotEmpty ? excelPoliciesNotOnTable : uploadMap.keys.map((k) => uploadPolicyDisplay[k] ?? k).toList(), invalidExcelRows: invalidRows, ); return; } final bool proceed = await _showUploadReviewProceedDialog( matchedRows: matchedRowsForDialog, excelPolicyNumbersNotOnTable: excelPoliciesNotOnTable, invalidExcelRows: invalidRows, ); if (!proceed) { ToastHelper.showWarningToast(context, 'Commission update cancelled'); return; } setState(() { /* * Excel upload action: * - update commission values for matched policy rows. * - capture optional UTR Number column per policy row. * captured UTR values are sent when user clicks "Raise Invoice". * - compare table utr_no vs excel UTR Number and refresh UI value. */ for (final policy in matchedPolicyRefs) { final key = _normalizePolicyNo(policy['policy_no']); if (uploadMap.containsKey(key)) { policy['commission_amount'] = uploadMap[key]!.toStringAsFixed(2); } if (uploadUtrMap.containsKey(key)) { _uploadedUtrByPolicyKey[key] = uploadUtrMap[key]!; policy['utr_no'] = uploadUtrMap[key]!; } } }); _calculateTotalCommission(); final message = 'Updated $matched policy commission(s) from Excel' '${utrComparedCount > 0 ? ' | UTR compared: $utrComparedCount (same: $utrSameCount, changed: $utrChangedCount)' : ''}' '${excelPoliciesNotOnTable.isNotEmpty ? ' | Excel-only (ignored): ${excelPoliciesNotOnTable.length}' : ''}' '${tableRowsNotInExcel > 0 ? ' | table rows not in Excel: $tableRowsNotInExcel' : ''}' '${invalidRows > 0 ? ' | invalid Excel rows: $invalidRows' : ''}'; ToastHelper.showSuccessToast(context, message); } catch (e) { ToastHelper.showErrorToast(context, e.toString()); } finally { if (mounted) { setState(() => isLoading = false); } } } 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, changes 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) { final df = DateFormat("dd-MM-yyyy"); final int status = int.tryParse(controllers['invoiceStatus']?.text ?? '0') ?? 0; final bool isPending = status == 1; return MainLayout( title: "Pay Out", body: !isLoadingEditData ? Container( color: Colors.white, padding: const EdgeInsets.all(8), margin: isEdit ? EdgeInsets.symmetric( horizontal: MediaQuery.of(context).size.width * 0.1, ) : null, 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( "Invoice", style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w500, ), ), // 🟢 Status Chip if (isEdit) ...[ Spacer(), Text( controllers['invoiceNo']?.text ?? '', style: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w500, color: const Color(0xFF374151), ), ), SizedBox(width: 10), Text( // controllers['invoiceDate']?.text ?? '', '(${controllers['invoiceDate']?.text ?? ''})', style: GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w300, color: const Color(0xFF6B7280), ), ), // SizedBox(width: 10), // Spacer(), SizedBox(width: 10), Text( 'Status: ${isPending ? 'Pending' : 'Completed'}', style: GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w300, color: const Color(0xFF6B7280), // color: isPending // ? Colors.blueAccent // : const Color(0xFF047857), ), ), Spacer(), ThemedSearchField( hintText: 'Search', // backgroundColor: Color(0xFFF6F8F8), onChanged: filterPolicyData, controller: _searchStaffController, backgroundColor: Color(0xFFFFFFFF), txtHeight: 30, txtwidth: ResponsiveLayout.isMobile(context) ? MediaQuery.of(context).size.width * 0.7 : MediaQuery.of(context).size.width * 0.15, ), ], if (!isEdit) ...[ const Spacer(), OutlinedButton.icon( onPressed: isLoading ? null : _triggerBulkUpload, icon: const Icon(Icons.upload_file, size: 16), label: Text( "Upload", style: GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w500, ), ), style: OutlinedButton.styleFrom( side: const BorderSide(color: Color(0xFF2E7D6E)), foregroundColor: const Color(0xFF2E7D6E), padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8, ), ), ), 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 COMMISSION :", 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), ], ], ], ), ), ), // SizedBox(height: 5), if (!isEdit) ...[ Container( padding: EdgeInsets.all(8.0), child: DateFilterRowPayout( formKey: _formKey, // key: ValueKey(SelectedStatus ?? ''), role: roleId, id: userId, selectedParnter: selectedAgentId, selectedBroker: selectedBrokerID, onBrokerChanged: (val) { print('Selected Filterd SselectedBrokerID - $val'); setState(() { selectedBrokerID = val; // ✅ parent state updated }); }, 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 - $selectedBrokerID', ); // 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: SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox( width: _tableMinWidth, child: Column( children: [ _policyTableHeader(), // static header _policyTableBody(), // vertical scroll rows ], ), ), ), ), ], ), ), if (selectedPolicies.isNotEmpty) ...[ Container( padding: const EdgeInsets.all(8), // color: Colors.amber, color: Color(0xFFf9fafb), // color: Colors.white, child: Row( // mainAxisAlignment: MainAxisAlignment.spaceBetween, 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 Commission", 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(), buildPOS(context), SizedBox(width: 20), // ---------------- BUTTONS ---------------- 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( "Raise Invoice", 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 (isEdit) ...[ Container( padding: const EdgeInsets.all(8), // color: Colors.amber, color: Color(0xFFf9fafb), // color: Colors.white, child: Row( mainAxisAlignment: MainAxisAlignment.end, // mainAxisAlignment: MainAxisAlignment.spaceBetween, 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 Commission", 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, ), ), const SizedBox(width: 10), ], ), ), ], // ---------------------- // const SizedBox(height: 100), ], ), ) : Container( color: Colors.white, height: MediaQuery.of(context).size.height * 0.7, child: Center(child: CircularProgressIndicator()), ), ); } 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( height: 30, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: const BoxDecoration( // color: Color(0xFFf9fafb), color: Color(0xFFf9fafb), // color: Color(0xFFf9fafb), // color: Color(0xFFF6F8F7), // border: Border(bottom: BorderSide(color: Color(0xFFE0E0E0))), // border: Border(bottom: BorderSide(color: Color(0xFFECEFF1))), border: Border(bottom: BorderSide(color: Color(0xFFf9fafb))), ), child: Row( children: [ if (!isEdit) ...[ SizedBox( width: colCheck, 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(); // selectedPolicies = filteredPolicies // .map( // (p) => int.tryParse(p["policy_id"].toString()) ?? 0, // ) // .toSet(); } else { selectedPolicies.clear(); } totalPolicies = selectedPolicies.length.toString(); }); _calculateTotalCommission(); }, ), ), ], _headerText("POLICY NO", colPolicy), _headerText("POLICY DATE", colDate), _headerText("REFERER", colAgent), _headerText("CUSTOMER", colCustomer), _headerText("PREMIUM", colPremium), _headerText("UTR NO", colUtr), _headerText("COMMISSION", colCommission), ], ), ); } Widget _headerText(String text, double width) { return SizedBox( width: width, child: Text( text, style: GoogleFonts.poppins( fontSize: 11, fontWeight: FontWeight.w500, color: Colors.blueGrey, letterSpacing: 0.5, ), ), ); } Widget _policyTableBody() { return SizedBox( // height: isEdit ? 400 : 340, // avoids overflow height: isEdit ? MediaQuery.of(context).size.height * 0.65 : MediaQuery.of(context).size.height * 0.48, // avoids overflow child: ListView.builder( itemCount: filteredPolicies.length, itemBuilder: (context, index) { final p = filteredPolicies[index]; final int id = int.tryParse(p["policy_id"]?.toString() ?? "0") ?? 0; final bool isSelected = selectedPolicies.contains(id); return InkWell( onTap: () { setState(() { isSelected ? selectedPolicies.remove(id) : selectedPolicies.add(id); totalPolicies = selectedPolicies.length.toString(); }); _calculateTotalCommission(); }, child: Container( height: 38, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( // const Color(0xFFE6F4EF) color: isSelected ? const Color(0xFFF0FAF6) : Colors.white, border: const Border( bottom: BorderSide(color: Color(0xFFf9fafb)), // bottom: BorderSide(color: Color(0xFFEDEDED)), ), ), child: Row( children: [ if (!isEdit) ...[ SizedBox( width: colCheck, child: Checkbox( value: isSelected, onChanged: (v) { setState(() { v == true ? selectedPolicies.add(id) : selectedPolicies.remove(id); totalPolicies = selectedPolicies.length.toString(); }); _calculateTotalCommission(); }, ), ), ], _cell(p["policy_no"], colPolicy), // _cell(p["issued_date"] ?? "-", colDate), _cell(_formatDate(p['issued_date']), colDate), // _cell(p["agent_name"] ?? "-", colAgent), _cell( (p["agent_code"] != null && p["agent_name"] != null) ? '${p["agent_code"]} - ${p["agent_name"]}' : (p["agent_name"] ?? '-'), colAgent, ), _cell(p["customer_name"], colCustomer), _cell("₹${p["premium_amount"]}", colPremium), _cell(_displayText(p["utr_no"]), colUtr), SizedBox( width: colCommission, child: Row( children: [ const Text( "₹", style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF009B77), ), ), const SizedBox(width: 2), Expanded( child: TextFormField( key: ValueKey( 'commission_${p["policy_id"]}_${p["commission_amount"]}', ), initialValue: (p["commission_amount"] ?? '') .toString(), style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF009B77), ), keyboardType: const TextInputType.numberWithOptions( decimal: true, ), inputFormatters: _commissionInputFormatter, decoration: const InputDecoration( isDense: true, contentPadding: EdgeInsets.symmetric( horizontal: 6, vertical: 6, ), border: OutlineInputBorder(), ), onChanged: (value) { p["commission_amount"] = value.trim(); _calculateTotalCommission(); }, ), ), ], ), ), ], ), ), ); }, ), ); } double _parseCommissionValue(dynamic value) { if (value == null) return 0; return double.tryParse(value.toString().trim()) ?? 0; } Widget _cell(String? value, double width) { return SizedBox( width: width, child: Text( value ?? "-", overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11), ), ); } 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 buildPOS(BuildContext context) { Map? selectedPOSdata = filteredPOSData.firstWhere( (item) => item['id'].toString() == selectedPOS, orElse: () => {}, ); return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('Select POS', style: _textStyle), SizedBox(width: 5), SizedBox( height: 35, width: MediaQuery.of(context).size.width * 0.15, child: DropdownSearch>( key: dropDownKeyPOS, selectedItem: selectedPOSdata.isNotEmpty ? selectedPOSdata : null, items: (filter, infiniteScrollProps) { return filteredPOSData; }, itemAsString: (val) => val['name'].toString(), // what to show compareFn: (item, selectedItem) => item['id'] == selectedItem['id'], // ✅ compare by id // validator: (val) { // if (val == null) { // return "Required"; // ✅ error message // } // return null; // }, suffixProps: DropdownSuffixProps( // make sure the dropdown button is visible dropdownButtonProps: DropdownButtonProps( isVisible: true, padding: EdgeInsets.zero, // remove default padding constraints: const BoxConstraints( // shrink icon tap area minWidth: 12, minHeight: 12, ), iconSize: 15, // smaller icon // icon: const Icon(Icons.arrow_drop_down), ), ), dropdownBuilder: (context, selectedItem) => Align( alignment: Alignment.centerLeft, child: Text( selectedItem != null ? selectedItem['name'].toString() : "", style: GoogleFonts.poppins( fontSize: 11, color: Colors.black, // color: Color(0XFF6366F1), ), overflow: TextOverflow.ellipsis, maxLines: 1, softWrap: false, ), ), decoratorProps: DropDownDecoratorProps( decoration: AppInputDecorations.dropdownDecoration(label: "POS") .copyWith( filled: true, fillColor: Colors.white, // 👈 makes the dropdown input white isDense: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: const BorderSide( color: Color(0xFFE2E8F0), width: 0.1, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: const BorderSide( color: Color(0xFFE2E8F0), width: 0.5, ), ), contentPadding: EdgeInsets.symmetric( horizontal: 8, vertical: 6, ), ), ), popupProps: PopupProps.menu( fit: FlexFit.loose, constraints: BoxConstraints(maxHeight: 250), menuProps: MenuProps( backgroundColor: Colors.white, // 👈 sets dropdown background to white ), showSearchBox: true, searchFieldProps: TextFieldProps( autofocus: true, style: GoogleFonts.inter(fontSize: 11, color: Colors.black), decoration: InputDecoration( contentPadding: EdgeInsets.all(1), filled: true, fillColor: Colors.white, hintText: "Search POS...", hintStyle: GoogleFonts.inter( fontSize: 12, color: Colors.black, ), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.white, ), // 👈 Normal border ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.white, width: 1.5, ), // 👈 Focused border ), ), ), itemBuilder: (context, item, isDisabled, isSelected) { return Container( // color: isSelected ? Colors.blue.withOpacity(0.1) : null, padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), child: Text( item['name'].toString(), style: GoogleFonts.inter(fontSize: 12, color: Colors.black), ), ); }, // constraints: BoxConstraints(), ), onChanged: (val) { if (val != null) { print("Selected POS : ${val['name']}"); print("Id: ${val['id']}"); selectedPOS = val['id']; // widget.onBrokerChanged?.call(val['id']); // controllers['agentId']?.text = val['agent_code']; // agentId = agent['id']; } }, ), ), ], ); } 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, ); 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 final double colCheck = 44; double get colPolicy => isEdit ? 150 : 200; double get colDate => isEdit ? 120 : 200; static final double colAgent = 200; static final double colCustomer = 200; static final double colPremium = 200; static final double colUtr = 170; // static final double colCommission = 200; double get colCommission => isEdit ? 120 : 200; double get _tableMinWidth => (isEdit ? 0 : colCheck) + colPolicy + colDate + colAgent + colCustomer + colPremium + colUtr + colCommission + 32; }