1072 lines
43 KiB
Dart
1072 lines
43 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.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 '../../providers/manager_provider.dart';
|
|
import 'FormFieldBox.dart';
|
|
|
|
class PayOutScreen extends ConsumerStatefulWidget {
|
|
final Map<String, dynamic>? editItem;
|
|
|
|
const PayOutScreen({super.key, this.editItem});
|
|
|
|
@override
|
|
ConsumerState<PayOutScreen> createState() => _PayOutScreenState();
|
|
}
|
|
|
|
class _PayOutScreenState extends ConsumerState<PayOutScreen> {
|
|
// --------------------------
|
|
// Invoice Fields
|
|
// --------------------------
|
|
late TextEditingController invoiceNoController;
|
|
DateTime invoiceDate = DateTime.now();
|
|
int? selectedAgentId;
|
|
int? selectedBrokerID;
|
|
DateTime? policyTillDate = DateTime.now();
|
|
|
|
// --------------------------
|
|
// Filters
|
|
// --------------------------
|
|
DateTime? fromDate;
|
|
DateTime? toDate;
|
|
|
|
// --------------------------
|
|
// 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;
|
|
late ApiService apiService;
|
|
dynamic managerId;
|
|
dynamic userId;
|
|
final DateFormat df = DateFormat('dd-MM-yyyy');
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
apiService = ApiService();
|
|
invoiceNoController = TextEditingController(text: generateInvoiceNumber());
|
|
Future.microtask(() async {
|
|
managerId = ref.watch(managerIdProvider);
|
|
userId = ref.watch(userIdProvider);
|
|
if (managerId != null) {
|
|
await getAgentList(managerId);
|
|
}
|
|
await getBroker();
|
|
});
|
|
|
|
// 👉 CHECK EDIT MODE
|
|
if (widget.editItem != null) {
|
|
print('editItemID ${widget.editItem}');
|
|
final invoiceID = widget.editItem!['id'];
|
|
_loadEditData(invoiceID);
|
|
}
|
|
}
|
|
|
|
void _loadEditData(String invoiceID) async {
|
|
setState(() => isLoading = true);
|
|
|
|
final response = await apiService.getInvoiceDetails(invoiceID);
|
|
|
|
if (response['status'] == 'success') {
|
|
final invoice = response['data']['invoice'];
|
|
final items = List<Map<String, dynamic>>.from(response['data']['items']);
|
|
|
|
print("Invoice → $invoice");
|
|
print("Items → $items");
|
|
|
|
// -----------------------------
|
|
// 1. SET BASIC FIELDS
|
|
// -----------------------------
|
|
setState(() {
|
|
selectedBrokerID = int.tryParse(invoice['broker_id'].toString());
|
|
selectedAgentId = int.tryParse(invoice['agent_id'].toString());
|
|
|
|
invoiceNoController.text = invoice['invoice_no'];
|
|
invoiceDate = DateTime.parse(invoice['invoice_date']);
|
|
|
|
if (invoice['till_date'] != "0000-00-00") {
|
|
policyTillDate = DateTime.parse(invoice['till_date']);
|
|
} else {
|
|
policyTillDate = DateTime.now();
|
|
}
|
|
});
|
|
print('selectedBrokerID $selectedBrokerID');
|
|
|
|
// -----------------------------
|
|
// 3. LOAD ALL POLICIES FOR THIS BROKER + AGENT
|
|
// -----------------------------
|
|
await loadPolicies();
|
|
|
|
// -----------------------------
|
|
// 2. SET SELECTED POLICIES
|
|
// -----------------------------
|
|
selectedPolicies = items
|
|
.map<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(() => isLoading = 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('getBroker called');
|
|
setState(() {
|
|
isLoading = true;
|
|
});
|
|
|
|
try {
|
|
final response = await apiService.fetchMasterDropDown('Broker');
|
|
|
|
if (response['status'] == 200) {
|
|
print('getBroker - ${response['data']}');
|
|
setState(() {
|
|
getBrokerData = List<Map<String, dynamic>>.from(response['data']);
|
|
print('API Data - $getBrokerData');
|
|
|
|
filteredBrokerData = List.from(getBrokerData);
|
|
print('originalData - $filteredBrokerData');
|
|
});
|
|
} else {
|
|
getBrokerData = [];
|
|
filteredBrokerData = [];
|
|
}
|
|
} catch (e) {
|
|
print('Exception occurred: $e');
|
|
} finally {
|
|
setState(() {
|
|
isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
//Get Agent List
|
|
Future<void> getAgentList(int id) async {
|
|
print('getClaimList called');
|
|
setState(() {
|
|
isLoading = true;
|
|
});
|
|
|
|
try {
|
|
final response = await apiService.fetchAgentUserList(managerId);
|
|
|
|
if (response['status'] == 'success') {
|
|
print('getAgentListData - ${response['data']}');
|
|
setState(() {
|
|
getAgentData = List<Map<String, dynamic>>.from(response['data']);
|
|
print('API Data - $getAgentData');
|
|
|
|
filteredAgentData = List.from(getAgentData);
|
|
print('originalData - $filteredAgentData');
|
|
});
|
|
} else {
|
|
getAgentData = [];
|
|
filteredAgentData = [];
|
|
}
|
|
} catch (e) {
|
|
print('Exception occurred: $e');
|
|
} finally {
|
|
setState(() {
|
|
isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Generate invoice number
|
|
String generateInvoiceNumber() {
|
|
final now = DateTime.now();
|
|
final random = now.millisecondsSinceEpoch % 1000;
|
|
return "INV${now.year}${now.month.toString().padLeft(2, '0')}${random.toString().padLeft(3, '0')}";
|
|
}
|
|
|
|
// Pick date helper
|
|
Future<DateTime?> pickDate(DateTime initial) async {
|
|
return await showDatePicker(
|
|
context: context,
|
|
initialDate: initial,
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime(2100),
|
|
);
|
|
}
|
|
|
|
// Load policies (simulate API)
|
|
Future<void> loadPolicies() async {
|
|
try {
|
|
String formattedTillDate = DateFormat(
|
|
'yyyy-MM-dd',
|
|
).format(policyTillDate!);
|
|
final jsondata = {
|
|
"broker_id": selectedBrokerID,
|
|
"agent_id": selectedAgentId,
|
|
"issued_date": formattedTillDate,
|
|
};
|
|
final response = await apiService.getCommissionRateList(jsondata);
|
|
|
|
if (response['status'] == 'success') {
|
|
print('API Data response - $response');
|
|
setState(() {
|
|
filteredPolicies = List<Map<String, dynamic>>.from(response['data']);
|
|
print('API Data - $filteredPolicies');
|
|
// filteredPolicies = allPolicies.where((p) {
|
|
// if (p["agentId"] != selectedAgentId) return false;
|
|
// if (policyTillDate != null &&
|
|
// DateTime.parse(p["date"]).isAfter(policyTillDate!))
|
|
// return false;
|
|
// return true;
|
|
// }).toList();
|
|
});
|
|
} else {
|
|
filteredPolicies = [];
|
|
}
|
|
} catch (e) {
|
|
print('Exception occurred: $e');
|
|
} 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 an agent");
|
|
return;
|
|
}
|
|
|
|
if (selectedPolicies.isEmpty) {
|
|
ToastHelper.showWarningToast(
|
|
context,
|
|
"Please select at least 1 policy",
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Format dates for API
|
|
final String formattedInvoiceDate = DateFormat(
|
|
'yyyy-MM-dd',
|
|
).format(invoiceDate);
|
|
final String formattedTillDate = DateFormat(
|
|
'yyyy-MM-dd',
|
|
).format(policyTillDate!);
|
|
|
|
// Total Commission
|
|
final double totalCommission = filteredPolicies
|
|
.where((p) => selectedPolicies.contains(int.parse(p["policy_id"])))
|
|
.fold(0.0, (sum, p) => sum + double.parse(p["commission_amount"]));
|
|
|
|
// Build items array
|
|
final List<Map<String, dynamic>> items = filteredPolicies
|
|
.where((p) => selectedPolicies.contains(int.parse(p["policy_id"])))
|
|
.map(
|
|
(p) => {
|
|
"policy_id": int.parse(p["policy_id"]),
|
|
"policy_no": p["policy_no"],
|
|
"commission_amount": double.parse(p["commission_amount"]),
|
|
},
|
|
)
|
|
.toList();
|
|
|
|
final jsondata = {
|
|
"invoice_no": invoiceNoController.text,
|
|
"invoice_amount": totalCommission,
|
|
"agent_id": selectedAgentId,
|
|
"broker_id": selectedBrokerID,
|
|
"invoice_date": formattedInvoiceDate,
|
|
"till_date": formattedTillDate,
|
|
"payout_status": 0,
|
|
"created_by": userId,
|
|
"updated_by": userId,
|
|
"items": items,
|
|
};
|
|
print('Commissions $jsondata');
|
|
final response = await apiService.getCreateOrUpdate(jsondata);
|
|
|
|
if (response['status'] == 'success') {
|
|
setState(() {
|
|
print('API Data response - $response');
|
|
selectedPolicies.clear();
|
|
invoiceNoController.clear();
|
|
invoiceDate = DateTime.now();
|
|
policyTillDate = DateTime.now();
|
|
filteredPolicies = [];
|
|
});
|
|
context.go(AppRoutes.invoiceList);
|
|
} else {
|
|
filteredPolicies = [];
|
|
}
|
|
} catch (e) {
|
|
print('Exception occurred: $e');
|
|
} finally {
|
|
setState(() {
|
|
isLoading = false;
|
|
});
|
|
}
|
|
|
|
// showDialog(
|
|
// context: context,
|
|
// builder: (_) => AlertDialog(
|
|
// title: const Text("Success"),
|
|
// content: Text(
|
|
// "Invoice No: ${invoiceNoController.text}\n"
|
|
// "Total Commission: ₹$totalComm",
|
|
// ),
|
|
// actions: [
|
|
// TextButton(
|
|
// onPressed: () => Navigator.pop(context),
|
|
// child: const Text("OK"),
|
|
// ),
|
|
// ],
|
|
// ),
|
|
// );
|
|
}
|
|
|
|
void showMessage(String msg) {
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final df = DateFormat("dd-MM-yyyy");
|
|
|
|
return MainLayout(
|
|
title: "Pay Out",
|
|
body: SelectionArea(
|
|
child: Stack(
|
|
children: [
|
|
SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
// ----------------------
|
|
// Invoice Details
|
|
// ----------------------
|
|
Card(
|
|
color: Colors.white, // White background
|
|
clipBehavior: Clip.none,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
elevation: 1,
|
|
child: Column(
|
|
children: [
|
|
// ---------- HEADER (CLICK TO COLLAPSE) ----------
|
|
ListTile(
|
|
title: const Text("Invoice Details"),
|
|
trailing: Icon(
|
|
invoiceCollapsed
|
|
? Icons.expand_more
|
|
: Icons.expand_less,
|
|
color: Color(0xFF2F766E),
|
|
size: 26,
|
|
),
|
|
onTap: () => setState(
|
|
() => invoiceCollapsed = !invoiceCollapsed,
|
|
),
|
|
),
|
|
// ------------------- HR LINE WHEN OPEN -------------------
|
|
if (!invoiceCollapsed)
|
|
const Divider(
|
|
height: 1,
|
|
thickness: 1,
|
|
color: Color(0xFFE0E0E0),
|
|
),
|
|
|
|
// ---------- COLLAPSIBLE CONTENT ----------
|
|
AnimatedCrossFade(
|
|
duration: const Duration(milliseconds: 500),
|
|
crossFadeState: invoiceCollapsed
|
|
? CrossFadeState.showFirst
|
|
: CrossFadeState.showSecond,
|
|
firstChild: const SizedBox(
|
|
// FIXED
|
|
height: 0,
|
|
width: double.infinity,
|
|
),
|
|
secondChild: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
// ------------------- ROW OF FIELDS -------------------
|
|
SizedBox(
|
|
height: 80, // 👈 ensures bottom alignment works
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment
|
|
.center, // 👈 bottom align everything
|
|
children: [
|
|
// INVOICE NO
|
|
FormFieldBox(
|
|
label: "Invoice No *",
|
|
child: TextField(
|
|
controller: invoiceNoController,
|
|
readOnly: true,
|
|
decoration: commonInputDecoration(
|
|
hint: "Invoice Number",
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// INVOICE DATE
|
|
FormFieldBox(
|
|
label: "Invoice Date *",
|
|
child: GestureDetector(
|
|
onTap: () async {
|
|
final picked = await pickDate(
|
|
invoiceDate,
|
|
);
|
|
if (picked != null)
|
|
setState(
|
|
() => invoiceDate = picked,
|
|
);
|
|
},
|
|
child: AbsorbPointer(
|
|
child: TextField(
|
|
readOnly: true,
|
|
decoration:
|
|
commonInputDecoration(
|
|
hint: df.format(invoiceDate),
|
|
).copyWith(
|
|
suffixIcon: const Icon(
|
|
Icons.calendar_today,
|
|
size: 18,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// BROKER
|
|
FormFieldBox(
|
|
label: "Select Broker *",
|
|
child: DropdownButtonFormField<int>(
|
|
value: selectedBrokerID,
|
|
decoration: commonInputDecoration(
|
|
hint: "Select Broker",
|
|
),
|
|
items: filteredBrokerData.map((a) {
|
|
final int id =
|
|
int.tryParse(
|
|
a['id'].toString(),
|
|
) ??
|
|
0;
|
|
return DropdownMenuItem<int>(
|
|
value: id,
|
|
child: Text(a['name'].toString()),
|
|
);
|
|
}).toList(),
|
|
onChanged: (id) => setState(
|
|
() => selectedBrokerID = id,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// AGENT
|
|
FormFieldBox(
|
|
label: "Select Agent *",
|
|
child: DropdownButtonFormField<int>(
|
|
value: selectedAgentId,
|
|
decoration: commonInputDecoration(
|
|
hint: "Select Agent",
|
|
),
|
|
items: filteredAgentData.map((a) {
|
|
final int id =
|
|
int.tryParse(
|
|
a['id'].toString(),
|
|
) ??
|
|
0;
|
|
return DropdownMenuItem<int>(
|
|
value: id,
|
|
child: Text(a['name'].toString()),
|
|
);
|
|
}).toList(),
|
|
onChanged: (id) => setState(
|
|
() => selectedAgentId = id,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// POLICY TILL DATE
|
|
FormFieldBox(
|
|
label: "Policy Till Date *",
|
|
child: GestureDetector(
|
|
onTap: () async {
|
|
final picked = await pickDate(
|
|
policyTillDate!,
|
|
);
|
|
if (picked != null)
|
|
setState(
|
|
() => policyTillDate = picked,
|
|
);
|
|
},
|
|
child: AbsorbPointer(
|
|
child: TextField(
|
|
readOnly: true,
|
|
decoration:
|
|
commonInputDecoration(
|
|
hint: df.format(
|
|
policyTillDate!,
|
|
),
|
|
).copyWith(
|
|
suffixIcon: const Icon(
|
|
Icons.calendar_today,
|
|
size: 18,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// --- RESET + SEARCH (BOTTOM ALIGNED) ---
|
|
Column(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.center,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
// RESET
|
|
SizedBox(
|
|
height: 30,
|
|
width: 30,
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.zero,
|
|
),
|
|
onPressed: () {
|
|
setState(() {
|
|
selectedBrokerID = null;
|
|
selectedAgentId = null;
|
|
fromDate = null;
|
|
toDate = null;
|
|
policyTillDate =
|
|
DateTime.now();
|
|
filteredPolicies = [];
|
|
selectedPolicies.clear();
|
|
});
|
|
},
|
|
child: const Icon(
|
|
Icons.refresh,
|
|
size: 18,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// SEARCH
|
|
SizedBox(
|
|
height: 30,
|
|
width: 30,
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.zero,
|
|
),
|
|
onPressed: loadPolicies,
|
|
child: const Icon(
|
|
Icons.search,
|
|
size: 18,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// ----------------------
|
|
// Policy Selection
|
|
// ----------------------
|
|
Card(
|
|
color: Colors.white, // White background
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
elevation: 1,
|
|
child: Column(
|
|
children: [
|
|
ListTile(
|
|
// contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|
title: const Text("Policy Selection"),
|
|
// ---------------- RIGHT SIDE (filters + collapse) ----------------
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// FROM DATE
|
|
SizedBox(
|
|
height: 30,
|
|
width: MediaQuery.of(context).size.width * 0.12,
|
|
child: GestureDetector(
|
|
onTap: () async {
|
|
final date = await pickDate(
|
|
fromDate ?? DateTime.now(),
|
|
);
|
|
if (date != null)
|
|
setState(() => fromDate = date);
|
|
},
|
|
child: AbsorbPointer(
|
|
child: TextField(
|
|
readOnly: true,
|
|
decoration:
|
|
commonInputDecoration(
|
|
hint: "From Date",
|
|
).copyWith(
|
|
// 👇 THIS is where you set formatted date
|
|
suffixIcon: const Icon(
|
|
Icons.calendar_today,
|
|
size: 18,
|
|
),
|
|
labelText: fromDate == null
|
|
? "From Date"
|
|
: df.format(fromDate!),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
|
|
// TO DATE
|
|
SizedBox(
|
|
height: 30,
|
|
width: MediaQuery.of(context).size.width * 0.12,
|
|
child: GestureDetector(
|
|
onTap: () async {
|
|
final date = await pickDate(
|
|
toDate ?? DateTime.now(),
|
|
);
|
|
if (date != null)
|
|
setState(() => toDate = date);
|
|
},
|
|
child: AbsorbPointer(
|
|
child: TextField(
|
|
readOnly: true,
|
|
decoration:
|
|
commonInputDecoration(
|
|
hint: "To Date",
|
|
).copyWith(
|
|
// 👇 THIS is where you set formatted date
|
|
suffixIcon: const Icon(
|
|
Icons.calendar_today,
|
|
size: 18,
|
|
),
|
|
labelText: toDate == null
|
|
? "To Date"
|
|
: df.format(toDate!),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
|
|
// RESET BUTTON
|
|
SizedBox(
|
|
height: 30,
|
|
width: 30,
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.zero,
|
|
),
|
|
onPressed: () {
|
|
setState(() {
|
|
fromDate = null;
|
|
toDate = null;
|
|
});
|
|
loadPolicies();
|
|
},
|
|
child: const Icon(Icons.refresh, size: 18),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
|
|
// SEARCH BUTTON
|
|
SizedBox(
|
|
height: 30,
|
|
width: 30,
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.zero,
|
|
),
|
|
onPressed: filterPolicies,
|
|
child: const Icon(Icons.search, size: 18),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
|
|
// COLLAPSE ICON
|
|
GestureDetector(
|
|
onTap: () => setState(
|
|
() => policyCollapsed = !policyCollapsed,
|
|
),
|
|
child: Icon(
|
|
policyCollapsed
|
|
? Icons.expand_more
|
|
: Icons.expand_less,
|
|
color: Colors.teal,
|
|
size: 26,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
onTap: () =>
|
|
setState(() => policyCollapsed = !policyCollapsed),
|
|
),
|
|
// ------------------- HR LINE WHEN OPEN -------------------
|
|
if (!policyCollapsed)
|
|
const Divider(
|
|
height: 1,
|
|
thickness: 1,
|
|
color: Color(0xFFE0E0E0),
|
|
),
|
|
|
|
AnimatedCrossFade(
|
|
duration: const Duration(milliseconds: 500),
|
|
crossFadeState: policyCollapsed
|
|
? CrossFadeState.showFirst
|
|
: CrossFadeState.showSecond,
|
|
firstChild: Container(),
|
|
secondChild: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
// ---------------- SELECT ALL ----------------
|
|
if (filteredPolicies.isNotEmpty)
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 8,
|
|
horizontal: 16,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Checkbox(
|
|
value:
|
|
filteredPolicies.isNotEmpty &&
|
|
filteredPolicies.every(
|
|
(p) => selectedPolicies.contains(
|
|
int.parse(p["policy_id"]),
|
|
),
|
|
),
|
|
onChanged: (v) {
|
|
setState(() {
|
|
if (v == true) {
|
|
selectedPolicies =
|
|
filteredPolicies
|
|
.map(
|
|
(p) => int.parse(
|
|
p["policy_id"],
|
|
),
|
|
)
|
|
.toSet();
|
|
} else {
|
|
selectedPolicies.clear();
|
|
}
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(width: 8),
|
|
const Text(
|
|
"Select All Policies",
|
|
style: TextStyle(fontSize: 16),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// ---------------- POLICY LIST ----------------
|
|
if (filteredPolicies.isEmpty)
|
|
const Padding(
|
|
padding: EdgeInsets.all(20),
|
|
child: Text("No policies found"),
|
|
)
|
|
else
|
|
...filteredPolicies.map((p) {
|
|
// final int id = int.parse(p["policy_id"]);
|
|
// final bool isSelected = selectedPolicies.contains(id);
|
|
|
|
// ---- SAFE ID PARSING ----
|
|
final String policyIdStr =
|
|
p["policy_id"]?.toString() ?? "0";
|
|
final int id = int.tryParse(policyIdStr) ?? 0;
|
|
|
|
final bool isSelected = selectedPolicies
|
|
.contains(id);
|
|
|
|
// ---- SAFE FIELDS ----
|
|
final String policyNo =
|
|
p["policy_no"]?.toString() ?? "-";
|
|
final String customer =
|
|
p["customer_name"]?.toString() ?? "-";
|
|
|
|
final String premiumStr =
|
|
p["premium_amount"]?.toString() ?? "0";
|
|
final double premium =
|
|
double.tryParse(premiumStr) ?? 0;
|
|
|
|
final String commissionStr =
|
|
p["commission_amount"]?.toString() ?? "0";
|
|
final double commission =
|
|
double.tryParse(commissionStr) ?? 0;
|
|
|
|
return ListTile(
|
|
tileColor: isSelected
|
|
? Colors.teal.shade50
|
|
: null,
|
|
leading: Checkbox(
|
|
value: isSelected,
|
|
onChanged: (v) {
|
|
setState(() {
|
|
if (v == true) {
|
|
selectedPolicies.add(id);
|
|
} else {
|
|
selectedPolicies.remove(id);
|
|
}
|
|
});
|
|
},
|
|
),
|
|
|
|
title: Text(policyNo),
|
|
|
|
subtitle: Text(
|
|
"Customer: ${customer} | "
|
|
"Premium: ₹${premium}",
|
|
),
|
|
|
|
trailing: Text(
|
|
"₹${commission}",
|
|
style: const TextStyle(
|
|
color: Colors.teal,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
|
|
onTap: () {
|
|
setState(() {
|
|
if (isSelected) {
|
|
selectedPolicies.remove(id);
|
|
} else {
|
|
selectedPolicies.add(id);
|
|
}
|
|
});
|
|
},
|
|
);
|
|
}).toList(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 100),
|
|
],
|
|
),
|
|
),
|
|
|
|
// ------------------------------
|
|
// Bottom Summary Bar
|
|
// ------------------------------
|
|
if (selectedPolicies.isNotEmpty)
|
|
Positioned(
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
child: Material(
|
|
elevation: 8,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
color: Colors.white,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
// ---------------- SELECTED COUNT ----------------
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"Selected Policies",
|
|
style: TextStyle(fontSize: 12),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
selectedPolicies.length.toString(),
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
// ---------------- TOTAL COMMISSION ----------------
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"Total Commission",
|
|
style: TextStyle(fontSize: 12),
|
|
),
|
|
const SizedBox(height: 4),
|
|
|
|
/// NULL-SAFE TOTAL COMMISSION CALCULATION
|
|
Text(
|
|
"₹${_calculateTotalCommission()}",
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.teal,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
// ---------------- BUTTONS ----------------
|
|
Row(
|
|
children: [
|
|
TextButton(
|
|
onPressed: () =>
|
|
setState(() => selectedPolicies.clear()),
|
|
child: const Text("Clear"),
|
|
),
|
|
const SizedBox(width: 10),
|
|
ElevatedButton(
|
|
onPressed: saveInvoice,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
child: const Text("Submit"),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
), ),
|
|
);
|
|
}
|
|
|
|
double _calculateTotalCommission() {
|
|
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;
|
|
|
|
// Safe commission amount
|
|
final String commStr = p["commission_amount"]?.toString() ?? "0";
|
|
final double comm = double.tryParse(commStr) ?? 0;
|
|
|
|
total += comm;
|
|
}
|
|
|
|
// Remove decimal points (.00)
|
|
return total.roundToDouble();
|
|
}
|
|
|
|
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,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|