570 lines
20 KiB
Dart
570 lines
20 KiB
Dart
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.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 '../../../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 '../../themes/indicators/export_btn.dart';
|
|
import '../../themes/indicators/search_field_theme.dart';
|
|
|
|
/// Read-only payout invoice + policy lines (opened from payout list “View”).
|
|
class PayOutDetailsView extends ConsumerStatefulWidget {
|
|
final Map<String, dynamic> viewItem;
|
|
|
|
const PayOutDetailsView({super.key, required this.viewItem});
|
|
|
|
@override
|
|
ConsumerState<PayOutDetailsView> createState() => _PayOutDetailsViewState();
|
|
}
|
|
|
|
class _PayOutDetailsViewState extends ConsumerState<PayOutDetailsView> {
|
|
final TextEditingController _searchStaffController = TextEditingController();
|
|
|
|
List<Map<String, dynamic>> masterPolicies = [];
|
|
List<Map<String, dynamic>> filteredPolicies = [];
|
|
|
|
bool isLoading = false;
|
|
late ApiService apiService;
|
|
dynamic managerId;
|
|
|
|
dynamic totalPolicies = '0';
|
|
dynamic totalCommission = '0';
|
|
|
|
int _rowsPerPage = 10;
|
|
int _currentPage = 1;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
apiService = ApiService();
|
|
Future.microtask(() {
|
|
managerId = ref.read(managerIdProvider);
|
|
_loadInvoicePolicies();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_searchStaffController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadInvoicePolicies() async {
|
|
final invoiceId = widget.viewItem['id'];
|
|
if (invoiceId == null) {
|
|
setState(() {
|
|
filteredPolicies = [];
|
|
masterPolicies = [];
|
|
});
|
|
return;
|
|
}
|
|
|
|
setState(() => isLoading = true);
|
|
try {
|
|
final jsondata = {
|
|
'invoice_id': invoiceId,
|
|
'manager_id': managerId,
|
|
};
|
|
final response = await apiService.getViewInvoiceList(jsondata);
|
|
if (response['status'] == 'success') {
|
|
final payload = response['data'];
|
|
List<Map<String, dynamic>> items = [];
|
|
Map<String, dynamic>? invoiceMap;
|
|
|
|
if (payload is Map) {
|
|
final m = Map<String, dynamic>.from(payload);
|
|
invoiceMap = m['invoice'] is Map
|
|
? Map<String, dynamic>.from(m['invoice'] as Map)
|
|
: null;
|
|
final rawItems = m['items'];
|
|
if (rawItems is List) {
|
|
items = rawItems
|
|
.map((e) => Map<String, dynamic>.from(e as Map))
|
|
.toList();
|
|
}
|
|
} else if (payload is List) {
|
|
items = List<Map<String, dynamic>>.from(payload);
|
|
}
|
|
|
|
num commissionSum = 0;
|
|
for (final row in items) {
|
|
commissionSum +=
|
|
num.tryParse(row['commission_amount']?.toString() ?? '0') ?? 0;
|
|
}
|
|
|
|
final totPolicy = (invoiceMap?['total_policies'] ?? items.length)
|
|
.toString();
|
|
final totalCommiss =
|
|
invoiceMap?['total_commission'] ?? commissionSum;
|
|
|
|
setState(() {
|
|
totalPolicies = totPolicy;
|
|
totalCommission = totalCommiss;
|
|
masterPolicies = items;
|
|
filteredPolicies = List<Map<String, dynamic>>.from(items);
|
|
_currentPage = 1;
|
|
});
|
|
} else {
|
|
setState(() {
|
|
masterPolicies = [];
|
|
filteredPolicies = [];
|
|
});
|
|
}
|
|
} catch (_) {
|
|
setState(() {
|
|
masterPolicies = [];
|
|
filteredPolicies = [];
|
|
});
|
|
} finally {
|
|
if (mounted) setState(() => isLoading = false);
|
|
}
|
|
}
|
|
|
|
void filterPolicyData(String query) {
|
|
final q = query.trim();
|
|
setState(() {
|
|
_currentPage = 1;
|
|
if (q.isEmpty) {
|
|
filteredPolicies = List.from(masterPolicies);
|
|
return;
|
|
}
|
|
filteredPolicies = masterPolicies.where((item) {
|
|
return (item['policy_no'] ?? '').toString().contains(q) ||
|
|
(item['customer_name'] ?? '').toString().contains(q) ||
|
|
(item['agent_name'] ?? '').toString().contains(q) ||
|
|
(item['insurer_name'] ?? '').toString().contains(q) ||
|
|
(item['premium_amount'] ?? '').toString().contains(q) ||
|
|
(item['commission_amount'] ?? '').toString().contains(q) ||
|
|
_formatDate(item['issued_date']?.toString() ?? '').contains(q);
|
|
}).toList();
|
|
});
|
|
}
|
|
|
|
String _displayText(dynamic value) {
|
|
if (value == null) return '-';
|
|
final text = value.toString().trim();
|
|
if (text.isEmpty || text.toLowerCase() == 'null') return '-';
|
|
return text;
|
|
}
|
|
|
|
String _invoiceDateLabel() {
|
|
final ui = widget.viewItem['invoice_date_ui_format'];
|
|
if (ui != null && ui.toString().trim().isNotEmpty) {
|
|
return ui.toString();
|
|
}
|
|
final raw = widget.viewItem['invoice_date'];
|
|
if (raw == null) return '-';
|
|
try {
|
|
return DateFormat('dd-MM-yyyy').format(DateTime.parse(raw.toString()));
|
|
} catch (_) {
|
|
return raw.toString();
|
|
}
|
|
}
|
|
|
|
String _formatDate(String? rawDate) {
|
|
if (rawDate == null || rawDate.isEmpty) return '-';
|
|
try {
|
|
return DateFormat('dd-MM-yyyy').format(DateTime.parse(rawDate));
|
|
} catch (_) {
|
|
return rawDate;
|
|
}
|
|
}
|
|
|
|
String _amountText(dynamic value) {
|
|
final text = _displayText(value);
|
|
if (text == '-') return '-';
|
|
return text.replaceAll('₹', '').trim();
|
|
}
|
|
|
|
Widget _headerText(
|
|
String text,
|
|
int flex, {
|
|
TextAlign align = TextAlign.left,
|
|
}) {
|
|
return Expanded(
|
|
flex: flex,
|
|
child: Text(
|
|
text,
|
|
textAlign: align,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.blueGrey,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _cell(
|
|
String? value,
|
|
int flex, {
|
|
TextAlign align = TextAlign.left,
|
|
Color? color,
|
|
FontWeight? fontWeight,
|
|
}) {
|
|
return Expanded(
|
|
flex: flex,
|
|
child: Text(
|
|
value ?? '-',
|
|
textAlign: align,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: color,
|
|
fontWeight: fontWeight,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _policyTableHeader() {
|
|
return Container(
|
|
height: 30,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFFf9fafb),
|
|
border: Border(bottom: BorderSide(color: Color(0xFFf9fafb))),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
_headerText('S.NO', 5, align: TextAlign.center),
|
|
_headerText('POLICY NO', 14),
|
|
_headerText('POLICY DATE', 10, align: TextAlign.center),
|
|
_headerText('PARTNER', 20),
|
|
_headerText('CUSTOMER', 16),
|
|
_headerText('PREMIUM (₹)', 11, align: TextAlign.right),
|
|
_headerText('PAYOUT (₹)', 11, align: TextAlign.right),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _policyRow(Map<String, dynamic> p, int index) {
|
|
return Container(
|
|
height: 38,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border(bottom: BorderSide(color: Color(0xFFf9fafb))),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
_cell('${index + 1}', 5, align: TextAlign.center),
|
|
_cell(p['policy_no']?.toString(), 14),
|
|
_cell(_formatDate(p['issued_date']?.toString()), 10, align: TextAlign.center),
|
|
_cell(
|
|
(p['agent_code'] != null && p['agent_name'] != null)
|
|
? '${p['agent_code']} - ${p['agent_name']}'
|
|
: (p['agent_name'] ?? '-').toString(),
|
|
20,
|
|
),
|
|
_cell(p['customer_name']?.toString(), 16),
|
|
_cell(
|
|
_amountText(p['premium_amount']),
|
|
11,
|
|
align: TextAlign.right,
|
|
),
|
|
_cell(
|
|
_amountText(p['commission_amount']),
|
|
11,
|
|
align: TextAlign.right,
|
|
color: const Color(0xFF009B77),
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
int get _totalPages {
|
|
if (filteredPolicies.isEmpty) return 1;
|
|
return (filteredPolicies.length / _rowsPerPage).ceil();
|
|
}
|
|
|
|
List<Map<String, dynamic>> get _paginatedPolicies {
|
|
if (filteredPolicies.isEmpty) return const <Map<String, dynamic>>[];
|
|
final start = (_currentPage - 1) * _rowsPerPage;
|
|
final end = (start + _rowsPerPage).clamp(0, filteredPolicies.length);
|
|
return filteredPolicies.sublist(start, end);
|
|
}
|
|
|
|
void _goToPage(int page) {
|
|
if (page < 1 || page > _totalPages) return;
|
|
setState(() => _currentPage = page);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final statusRaw = widget.viewItem['payout_status'];
|
|
final statusInt = int.tryParse(statusRaw?.toString() ?? '0') ?? 0;
|
|
final isPending = statusInt == 1;
|
|
final invoiceNo = _displayText(widget.viewItem['invoice_no']);
|
|
|
|
return MainLayout(
|
|
title: 'Pay Out',
|
|
body: isLoading
|
|
? Container(
|
|
color: Colors.white,
|
|
height: MediaQuery.of(context).size.height * 0.7,
|
|
child: const Center(child: CircularProgressIndicator()),
|
|
)
|
|
: Container(
|
|
color: Colors.white,
|
|
padding: const EdgeInsets.all(8),
|
|
// margin: EdgeInsets.symmetric(
|
|
// horizontal: MediaQuery.of(context).size.width * 0.1,
|
|
// ),
|
|
child: Column(
|
|
children: [
|
|
SizedBox(
|
|
height: 40,
|
|
width: MediaQuery.of(context).size.width,
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
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),
|
|
Text(
|
|
'View Payout Details',
|
|
style: GoogleFonts.inter(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Text(
|
|
invoiceNo,
|
|
style: GoogleFonts.inter(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
color: const Color(0xFF374151),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Text(
|
|
'(${_invoiceDateLabel()})',
|
|
style: GoogleFonts.inter(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w300,
|
|
color: const Color(0xFF6B7280),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Text(
|
|
'Status: ${isPending ? 'Pending' : 'Completed'}',
|
|
style: GoogleFonts.inter(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w300,
|
|
color: const Color(0xFF6B7280),
|
|
),
|
|
),
|
|
const Spacer(),
|
|
ThemedSearchField(
|
|
hintText: 'Search',
|
|
onChanged: filterPolicyData,
|
|
controller: _searchStaffController,
|
|
backgroundColor: const Color(0xFFFFFFFF),
|
|
txtHeight: 30,
|
|
txtwidth: ResponsiveLayout.isMobile(context)
|
|
? MediaQuery.of(context).size.width * 0.7
|
|
: MediaQuery.of(context).size.width * 0.15,
|
|
),
|
|
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',
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Card(
|
|
color: Colors.white,
|
|
clipBehavior: Clip.antiAlias,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
elevation: 0,
|
|
child: ScrollConfiguration(
|
|
behavior: const MaterialScrollBehavior().copyWith(
|
|
dragDevices: {
|
|
PointerDeviceKind.mouse,
|
|
PointerDeviceKind.touch,
|
|
PointerDeviceKind.trackpad,
|
|
},
|
|
),
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final rows = _paginatedPolicies;
|
|
final startIdx = (filteredPolicies.isEmpty)
|
|
? 0
|
|
: ((_currentPage - 1) * _rowsPerPage);
|
|
|
|
return Column(
|
|
children: [
|
|
_policyTableHeader(),
|
|
Expanded(
|
|
child: filteredPolicies.isEmpty
|
|
? const Center(
|
|
child: Text(
|
|
'No policies in this payout',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.blueGrey,
|
|
),
|
|
),
|
|
)
|
|
: ListView.builder(
|
|
itemCount: rows.length,
|
|
itemBuilder: (context, index) {
|
|
return _policyRow(
|
|
rows[index],
|
|
startIdx + index,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
color: const Color(0xFFf9fafb),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
Text(
|
|
'$totalPolicies',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.blueGrey,
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'Policies',
|
|
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),
|
|
Text(
|
|
'₹ $totalCommission',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.teal,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (filteredPolicies.isNotEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(8, 4, 8, 2),
|
|
child: Align(
|
|
alignment: Alignment.centerRight,
|
|
child: PaginationControls(
|
|
currentPage: _currentPage,
|
|
itemsPerPage: _rowsPerPage,
|
|
totalItems: filteredPolicies.length,
|
|
onPageChanged: (page) => _goToPage(page),
|
|
onItemsPerPageChanged: (items) {
|
|
setState(() {
|
|
_rowsPerPage = items;
|
|
_currentPage = 1;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|