nhance_partner/lib/presentation/screens/payout/payout_details.dart
2026-04-06 17:23:16 +05:30

1553 lines
53 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 'dart:math' show max;
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 'custom_dateRange.dart';
class PayOutDetails extends ConsumerStatefulWidget {
const PayOutDetails({super.key});
@override
ConsumerState<PayOutDetails> createState() => _PayOutDetailsState();
}
class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
static final List<TextInputFormatter> _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 (1222).
static final RegExp _utrFullValuePattern =
RegExp(r'^[a-zA-Z0-9]{12,22}$');
static final List<TextInputFormatter> _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<FormState>();
final _utrFormKey = 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';
List<Map<String, dynamic>> masterPolicies = [];
// --------------------------
// 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 hasFetchedTableData = false;
int currentPage = 1;
int itemsPerPage = 10;
final Set<int> _inlineSavingIds = <int>{};
/// Per-policy payout editors so [setState] rebuilds do not reset focus/text.
final Map<int, TextEditingController> _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<String, dynamic> p) {
return int.tryParse(p["policy_id"]?.toString() ?? "0") ?? 0;
}
void _rebuildCommissionControllers(List<Map<String, dynamic>> 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<String, dynamic> p) {
final id = _policyRowId(p);
if (id == 0) return null;
return _commissionControllers[id];
}
Widget _commissionAmountField(
Map<String, dynamic> 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.right,
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();
}
void filterPolicyData(String query) {
final lowerQuery = query.toLowerCase();
print('PD => filterPolicyData - $lowerQuery');
print('PD => filteredPolicies1 - $filteredPolicies');
setState(() {
currentPage = 1;
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']?.toString() ?? '')
.toLowerCase()
.contains(lowerQuery);
}).toList();
});
}
void refresh() {
setState(() {
for (var controller in controllers.values) {
controller.clear();
}
selectedBrokerID = '';
selectedAgentId = [];
selectedPolicies = {};
totalPolicies = '';
totalCommission = '';
filteredPolicies = [];
currentPage = 1;
_disposeCommissionControllers();
hasFetchedTableData = false;
// dropDownKeyPartner.currentState?.clear();
});
}
// 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<String> 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(() {
filteredPolicies = List<Map<String, dynamic>>.from(response['data']);
_rebuildCommissionControllers(filteredPolicies);
hasFetchedTableData = true;
currentPage = 1;
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 = [];
_rebuildCommissionControllers(filteredPolicies);
hasFetchedTableData = true;
currentPage = 1;
});
}
} catch (e) {
print('PD =>Exception occurred: $e');
setState(() {
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<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 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<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;
});
}
}
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) {
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,
policyTableHasRows: filteredPolicies.isNotEmpty,
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: max(
_tableMinWidth,
MediaQuery.of(context).size.width - 32,
),
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(
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(
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: [
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();
} else {
selectedPolicies.clear();
}
totalPolicies = selectedPolicies.length.toString();
});
_calculateTotalCommission();
},
),
),
_headerText("S.NO", colSno, align: TextAlign.center),
_headerText("POLICY NO", colPolicy),
_headerText("POLICY DATE", colDate),
_headerText("PARTNER", colAgent),
_headerText("CUSTOMER", colCustomer),
_headerText("PREMIUM (₹)", colPremium, align: TextAlign.right),
const SizedBox(width: 10),
_headerText("PAYOUT (₹)", colCommission, align: TextAlign.right),
],
),
);
}
Widget _headerText(String text, double width, {TextAlign align = TextAlign.left}) {
return SizedBox(
width: width,
child: Text(
text,
textAlign: align,
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.w500,
color: Colors.blueGrey,
letterSpacing: 0.5,
),
),
);
}
Widget _policyTableBody() {
final pageRows = _paginatedPolicies;
final startIndex =
filteredPolicies.isEmpty ? 0 : (currentPage - 1) * itemsPerPage;
return SizedBox(
height: MediaQuery.of(context).size.height * 0.48,
child: ListView.builder(
itemCount: pageRows.length,
itemBuilder: (context, index) {
final p = pageRows[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: [
SizedBox(
width: colCheck,
child: Checkbox(
value: isSelected,
onChanged: (v) {
setState(() {
v == true
? selectedPolicies.add(id)
: selectedPolicies.remove(id);
totalPolicies = selectedPolicies.length.toString();
});
_calculateTotalCommission();
},
),
),
_cell(
'${startIndex + index + 1}',
colSno,
align: TextAlign.center,
),
_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(
_amountText(p["premium_amount"]),
colPremium,
align: TextAlign.right,
),
const SizedBox(width: 10),
SizedBox(
width: colCommission,
child: Row(
children: [
Expanded(
child: _commissionAmountField(
p,
_commissionControllerFor(p),
),
),
],
),
),
],
),
),
);
},
),
);
}
String _amountText(dynamic value) {
final raw = (value ?? '').toString().replaceAll('', '').trim();
if (raw.isEmpty || raw.toLowerCase() == 'null') return '-';
return raw;
}
List<Map<String, dynamic>> get _paginatedPolicies {
if (filteredPolicies.isEmpty) return const <Map<String, dynamic>>[];
final start = (currentPage - 1) * itemsPerPage;
final end = (start + itemsPerPage).clamp(0, filteredPolicies.length);
return filteredPolicies.sublist(start, end);
}
List<Map<String, dynamic>> get _exportRows {
return filteredPolicies.map((p) {
return {
'sno': 0,
'policy_no': p['policy_no'],
'policy_date': _formatDate((p['issued_date'] ?? '').toString()),
'partner': (p["agent_code"] != null && p["agent_name"] != null)
? '${p["agent_code"]} - ${p["agent_name"]}'
: (p["agent_name"] ?? '-').toString(),
'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<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;
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);
});
}
}
}
Widget _cell(
dynamic value,
double width, {
TextAlign align = TextAlign.left,
}) {
return SizedBox(
width: width,
child: Text(
(value ?? "-").toString(),
textAlign: align,
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) {
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,
);
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;
static final double colSno = 60;
static final double colPolicy = 200;
static final double colDate = 200;
static final double colAgent = 400;
static final double colCustomer = 200;
static final double colPremium = 200;
static final double colPremiumPayoutGap = 10;
static final double colCommission = 150;
double get _tableMinWidth =>
colCheck +
colSno +
colPolicy +
colDate +
colAgent +
colCustomer +
colPremium +
colPremiumPayoutGap +
colCommission +
32;
}