nhance_partner/lib/presentation/screens/payout/payout_details.dart
2026-04-04 09:14:15 +05:30

1632 lines
57 KiB
Dart

import 'dart:convert';
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 PayOutDetails extends ConsumerStatefulWidget {
final Map<String, dynamic>? editItem;
const PayOutDetails({super.key, this.editItem});
@override
ConsumerState<PayOutDetails> createState() => _PayOutDetailsState();
}
class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
static final List<TextInputFormatter> _commissionInputFormatter = [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')),
];
static final List<TextInputFormatter> _utrInputFormatter = [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9]')),
];
// --------------------------
// Invoice Fields
// --------------------------
final _formKey = GlobalKey<FormState>();
// final TextEditingController invoiceNoController;
DateTime invoiceDate = DateTime.now();
// dynamic selectedAgentId;
List<dynamic>? selectedAgentId;
dynamic selectedBrokerID;
DateTime? policyTillDate = DateTime.now();
dynamic totalPolicies = '0';
dynamic totalCommission = '0';
bool isEdit = false;
List<Map<String, dynamic>> masterPolicies = [];
final TextEditingController _searchStaffController = TextEditingController();
// --------------------------
// Filters
// --------------------------
DateTime? fromDate;
DateTime? toDate;
Map<String, TextEditingController> controllers = {};
List<String> tabHeader = [
'startDate',
'endDate',
'invoiceNo',
'invoiceDate',
'invoiceStatus',
'utrNumber',
];
// --------------------------
// Collapse States
// --------------------------
bool invoiceCollapsed = false;
bool policyCollapsed = false;
// --------------------------
// Sample Data (replace with API later)
// --------------------------
final List<Map<String, dynamic>> allPolicies = [];
List<Map<String, dynamic>> filteredPolicies = [];
Set<int> selectedPolicies = {};
List<Map<String, dynamic>> getBrokerData = [];
List<Map<String, dynamic>> filteredBrokerData = [];
List<Map<String, dynamic>> getAgentData = [];
List<Map<String, dynamic>> filteredAgentData = [];
bool isLoading = false;
bool isLoadingEditData = false;
bool hasFetchedTableData = false;
final Set<int> _inlineSavingIds = <int>{};
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();
// ✅ EDIT MODE (MUST be here)
if (widget.editItem != null) {
print('PD => editItemID ${widget.editItem}');
final invoiceID = widget.editItem!['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, managerId); // ✅ managerId ready
}
});
}
void filterPolicyData(String query) {
final lowerQuery = query.toLowerCase();
print('PD => filterPolicyData - $lowerQuery');
print('PD => filteredPolicies1 - $filteredPolicies');
setState(() {
if (query.trim().isEmpty) {
print('PD => filteredPolicies2');
filteredPolicies = List.from(masterPolicies);
return;
}
print('PD => 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();
});
}
void refresh() {
setState(() {
for (var controller in controllers.values) {
controller.clear();
controller.dispose();
}
selectedBrokerID = '';
selectedAgentId = [];
selectedPolicies = {};
totalPolicies = '';
totalCommission = '';
filteredPolicies = [];
hasFetchedTableData = false;
// dropDownKeyPartner.currentState?.clear();
});
}
void _loadEditData(String invoiceID, managerID) async {
setState(() => isLoadingEditData = true);
print("H010 => r : $roleId | mId: $managerID | ParmMID: $managerID | uId: $userId ");
print('PD => _loadEditData');
final jsondata = {
// "id": invoiceID,
"invoice_id": invoiceID,
"manager_id": managerId
// "issued_date": formattedTillDate,
};
print('PD => _loadEditData - $jsondata');
final response = await apiService.getCommissionRateList(jsondata);
// final response = await apiService.getInvoiceDetails(invoiceID);
if (response['status'] == 'success') {
print('PD => _loadEditData success');
print('PD => _loadEditData success - ${response['data']}');
final invoice = response['data'];
print("Invoice → $invoice");
// final items = List<Map<String, dynamic>>.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<Map<String, dynamic>> items = (response['data'] as List)
.map((e) => Map<String, dynamic>.from(e))
.toList();
masterPolicies = List<Map<String, dynamic>>.from(response['data']);
filteredPolicies = List<Map<String, dynamic>>.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<String>.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('PD => selectedBrokerID $selectedBrokerID');
// -----------------------------
// 3. LOAD ALL POLICIES FOR THIS BROKER + AGENT
// -----------------------------
// await loadPolicies();
// -----------------------------
// 2. SET SELECTED POLICIES
// -----------------------------
// selectedPolicies = items
// .map<int>((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<void> 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<Map<String, dynamic>>.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<void> 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<Map<String, dynamic>>.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<DateTime?> 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<void> loadPolicies() async {
print('PD =>loadPolicies');
print("H13 => r : $roleId | mId: $managerId | uId: $userId ");
setState(() {
isLoading = true;
hasFetchedTableData = false;
});
try {
final List<int> agentIds = (selectedAgentId ?? [])
.map((e) => int.parse(e))
.toList();
final jsondata = {
"from_date": controllers['startDate']?.text,
"to_date": controllers['endDate']?.text,
"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('PD =>API Data response - $response');
setState(() {
filteredPolicies = List<Map<String, dynamic>>.from(response['data']);
hasFetchedTableData = true;
print('PD =>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('PD =>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<void> 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 String utrInput = (controllers['utrNumber']?.text ?? '').trim();
if (utrInput.isEmpty) {
ToastHelper.showWarningToast(
context,
"Please enter the UTR number. It cannot be left empty.",
);
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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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;
});
}
}
String _displayText(dynamic value) {
if (value == null) return '-';
final text = value.toString().trim();
if (text.isEmpty || text.toLowerCase() == 'null') return '-';
return text;
}
List<Map<String, dynamic>> _buildUtrPayloadForSelectedPolicies({
required String defaultUtrDate,
}) {
final Map<String, double> 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) => <String, dynamic>{
"utr_no": e.key,
"amount": e.value,
"utr_date": defaultUtrDate,
},
)
.toList();
}
void showMessage(String msg) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
}
Future<bool> showSaveConfirmation(BuildContext context) async {
return await showDialog<bool>(
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) {
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(
"Payout Details",
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(),
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),
],
],
],
),
),
),
// SizedBox(height: 5),
if (!isEdit) ...[
Container(
padding: EdgeInsets.all(8.0),
child: DateFilterRowPayout(
formKey: _formKey,
// key: ValueKey(SelectedStatus ?? ''),
role: roleId,
id: userId,
showBrokerFilter: false,
selectedParnter: selectedAgentId,
selectedBroker: selectedBrokerID,
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: 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 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 ----------------
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 (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 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,
),
),
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("PARTNER", colAgent),
_headerText("CUSTOMER", colCustomer),
_headerText("PREMIUM", colPremium),
_headerText("PAYOUT", 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),
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();
},
onFieldSubmitted: (value) =>
_updateInlineCommission(p, value),
),
),
],
),
),
],
),
),
);
},
),
);
}
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<Map<String, dynamic>> 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<void> _updateInlineCommission(Map<String, dynamic> 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;
_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);
});
}
}
}
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 buildUTRNumber(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('UTR Number', style: _textStyle),
SizedBox(width: 5),
SizedBox(
height: 35,
width: MediaQuery.of(context).size.width * 0.15,
child: TextFormField(
controller: controllers['utrNumber']!,
inputFormatters: _utrInputFormatter,
decoration: commonInputDecoration(hint: 'UTR Number'),
),
),
],
);
}
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 colCommission = 200;
double get colCommission => isEdit ? 120 : 200;
double get _tableMinWidth =>
(isEdit ? 0 : colCheck) +
colPolicy +
colDate +
colAgent +
colCustomer +
colPremium +
colCommission +
32;
}