1553 lines
53 KiB
Dart
1553 lines
53 KiB
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 '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 (12–22).
|
||
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;
|
||
} |