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 'package:nhance_partner/data/utils/toastNotification.dart'; import '../../../core/routing/routes.dart'; import '../../../core/services/api_service.dart'; import '../../../data/utils/Pagination.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 '../../themes/indicators/search_field_theme.dart'; import '../../widgets/custom_Stdate_EnDate_Filter.dart'; class PayoutList extends ConsumerStatefulWidget { const PayoutList({super.key}); @override ConsumerState createState() => _PayoutListState(); } class _PayoutListState extends ConsumerState { static const double _payoutFilterFieldHeight = 35; int currentPage = 1; int itemsPerPage = 10; bool isLoading = false; List> getPayoutData = []; List> originalData = []; List> filteredData = []; dynamic roleId; dynamic userId; late ApiService apiService; final _formKey = GlobalKey(); Map controllers = {}; List tabHeader = [ 'startDate', 'endDate', 'agentName', 'agentCode', 'policyNo', ]; dynamic SelectedStatus; dynamic SelectedStaffId; DateTime _paymentDate = DateTime.now(); dynamic _hoveredHistoryInvoiceId; static const double _actionIconSize = 12; static const double _actionButtonSize = 28; static const double _actionIconGap = 2; @override void initState() { super.initState(); apiService = ApiService(); for (String field in tabHeader) { controllers[field] = TextEditingController(); } Future.microtask(() { final id = ref.read(managerIdProvider); roleId = ref.read(userRoleProvider); userId = ref.read(userIdProvider); print("PL 1 => r : $roleId | mId: $id | uId: $userId "); getPayoutList(); }); } // Get Payout List Future getPayoutList() async { print('PL 2 => get Payout Listing Called'); setState(() { isLoading = true; }); try { final from = controllers['startDate']?.text.trim() ?? ''; final to = controllers['endDate']?.text.trim() ?? ''; final response = await apiService.getPayoutList( fromDate: from.isNotEmpty ? from : null, toDate: to.isNotEmpty ? to : null, agentName: controllers['agentName']?.text, agentCode: controllers['agentCode']?.text, policyNo: controllers['policyNo']?.text, ); if (response['status'] == 'success') { print('PL 3 => getPayoutList - ${response['data']}'); final data = response['data']; print(' PL 4 => getPayoutList => ${response['data']}'); setState(() { currentPage = 1; if (data is List) { // Already a list of maps getPayoutData = List>.from(data); } else if (data is Map) { // Single object, wrap in a list getPayoutData = [Map.from(data)]; } else { getPayoutData = []; } originalData = getPayoutData; filteredData = List.from(originalData); }); } else { originalData = []; filteredData = []; } } catch (e) { print('PL 5 => Exception occurred: $e'); } finally { setState(() { isLoading = false; }); } } // Delete Invoice List Future deleteInvoice(ID) async { setState(() { isLoading = true; }); try { final response = await apiService.deleteInvoice(ID); if (response['status'] == 'success') { ToastHelper.showSuccessToast(context, response['data']); getPayoutList(); } else { ToastHelper.showSuccessToast(context, response['data']); } } catch (e) { print('PL 6 => Exception occurred: $e'); } finally { setState(() { isLoading = false; }); } } List get _paginatedData { // Sort descending by id first final sortedData = [...filteredData] ..sort((a, b) => int.parse(b['id']) - int.parse(a['id'])); if (sortedData.isEmpty) return []; // Ensure currentPage is valid final maxPage = (sortedData.length / itemsPerPage).ceil(); final safePage = currentPage.clamp(1, maxPage); final startIndex = (safePage - 1) * itemsPerPage; final endIndex = (startIndex + itemsPerPage).clamp(0, sortedData.length); return sortedData.sublist(startIndex, endIndex); } void filterDateRange() { getPayoutList(); } void refrshfilterDateRange() { setState(() { controllers['startDate']!.clear(); controllers['endDate']!.clear(); controllers['agentName']!.clear(); controllers['agentCode']!.clear(); controllers['policyNo']!.clear(); _formKey.currentState?.reset(); }); getPayoutList(); } void filterData(String query) { print("PL 7 =>FilterDAta - $query"); final q = query.toLowerCase(); setState(() { filteredData = getPayoutData.where((item) { // Search matches visible table columns only. final invoiceDateSearch = _formatDateSafe(item['invoice_date']) .toLowerCase(); final statusSearch = _exportPayoutStatusLabel(item['payout_status']).toLowerCase(); return invoiceDateSearch.contains(q) || (item['agent_name'] ?? '-').toString().toLowerCase().contains(q) || (item['agent_code'] ?? '-').toString().toLowerCase().contains(q) || (item['policy_no'] ?? '-').toString().toLowerCase().contains(q) || (item['invoice_no'] ?? '-').toLowerCase().contains(q) || (item['invoice_amount_indian_format'] ?? '-') .toLowerCase() .contains(q) || (item['utr_numbers'] ?? '-').toLowerCase().contains(q) || (item['invoiced_amount'] ?? '0').toLowerCase().contains(q) || (item['payout_amount'] ?? '0').toLowerCase().contains(q) || (item['balance_amount'] ?? '0').toLowerCase().contains(q) || statusSearch.contains(q) || // Also match raw codes if user types "1" or "2" (item['payout_status'] ?? '-').toString().toLowerCase().contains(q); }).toList(); }); } String _formatDateSafe(dynamic raw) { if (raw == null) return '-'; try { final dateTime = DateTime.parse(raw.toString()); return DateFormat('dd-MM-yyyy').format(dateTime); } catch (_) { return raw.toString(); } } final TextEditingController _searchStaffController = TextEditingController(); 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 } } /// Excel export: match table (1 = Pending, 2 = Completed). String _exportPayoutStatusLabel(dynamic v) { if (v == null) return '-'; final s = v.toString().trim(); if (s.isEmpty || s.toLowerCase() == 'null') return '-'; if (s == '1') return 'Pending'; if (s == '2') return 'Completed'; return 'Completed'; } String _displayText(dynamic value) { if (value == null) return '-'; final text = value.toString().trim(); if (text.isEmpty || text.toLowerCase() == 'null') return '-'; return text; } String _truncateText(String value, {int maxChars = 25}) { if (value.length <= maxChars) return value; return '${value.substring(0, maxChars)}...'; } String _utrTooltipText(dynamic value) { final text = _displayText(value); if (text == '-') return text; final items = text .split(',') .map((e) => e.trim()) .where((e) => e.isNotEmpty) .toList(); if (items.isEmpty) return text; return items.join('\n'); } String _displayAmount(dynamic value) { if (value == null) return '0'; final text = value.toString().trim(); if (text.isEmpty || text.toLowerCase() == 'null') return '0'; return text; } double _toDouble(dynamic value) { if (value == null) return 0; return double.tryParse(value.toString()) ?? 0; } String _generateUtrNumber() { final now = DateTime.now(); return 'UTR${DateFormat('yyyyMMddHHmmss').format(now)}'; } Future _showAddPaymentDialog(Map item) async { final TextEditingController amountController = TextEditingController(); _paymentDate = DateTime.now(); dynamic selectedUtrId; List> utrDetails = []; String generatedUtrNo = _generateUtrNumber(); double totalAmount = _toDouble(item['invoiced_amount']); double paidToDate = _toDouble(item['payout_amount']); double currentBalance = _toDouble(item['balance_amount']); double selectedUtrExistingPaid = 0; double maxAllowedForSelectedUtr = 0; try { final utrResponse = await apiService.getInvoiceUtrDetails(item['id']); if (utrResponse['status'] == 'success') { final dataRoot = utrResponse['data'] is Map ? Map.from(utrResponse['data']) : {}; final invoiceSummary = dataRoot['invoice'] is Map ? Map.from(dataRoot['invoice']) : {}; totalAmount = _toDouble(invoiceSummary['total_amount']); paidToDate = _toDouble(invoiceSummary['paid_to_date']); currentBalance = _toDouble(invoiceSummary['balance_amount']); utrDetails = List>.from(dataRoot['utrs_details'] ?? []); if (utrDetails.isEmpty) { generatedUtrNo = _generateUtrNumber(); } } } catch (e) { ToastHelper.showWarningToast( context, 'Failed to load UTR list. Please refresh and try again.', ); print('PL 8 => UTR load error: $e'); } final result = await showDialog( context: context, barrierDismissible: false, builder: (ctx) { return StatefulBuilder( builder: (ctx, setLocalState) { return AlertDialog( title: Text( 'Add Paid Amount', style: GoogleFonts.poppins(fontSize: 16, fontWeight: FontWeight.w600), ), content: SizedBox( width: 360, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Invoice number #${_displayText(item['invoice_no'])}', style: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), Row( children: [ Expanded( child: Text( 'Total Amount', style: GoogleFonts.inter(fontSize: 12), ), ), Text(': ${totalAmount.toStringAsFixed(2)}', style: GoogleFonts.inter(fontSize: 12)), ], ), const SizedBox(height: 6), Row( children: [ Expanded( child: Text( 'Amount Paid Till Date', style: GoogleFonts.inter(fontSize: 12), ), ), Text(': ${paidToDate.toStringAsFixed(2)}', style: GoogleFonts.inter(fontSize: 12)), ], ), const SizedBox(height: 8), const Divider(thickness: 1), const SizedBox(height: 8), Row( children: [ Expanded( child: Text( 'Balance Amount', style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w600, ), ), ), Text( ': ${currentBalance.toStringAsFixed(2)}', style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w600, ), ), ], ), const SizedBox(height: 12), if (currentBalance > 0) ...[ if (utrDetails.isNotEmpty) DropdownButtonFormField( initialValue: selectedUtrId, isExpanded: true, decoration: const InputDecoration( labelText: 'UTR Number', border: OutlineInputBorder(), ), items: utrDetails.map((utr) { return DropdownMenuItem( value: utr['id'], child: Text(_displayText(utr['utr_no'])), ); }).toList(), onChanged: (value) { final selected = utrDetails.firstWhere( (row) => '${row['id']}' == '$value', orElse: () => {}, ); final selectedPaid = _toDouble(selected['paid_amount']); final selectedDate = selected['utr_date']; setLocalState(() { selectedUtrId = value; selectedUtrExistingPaid = selectedPaid; maxAllowedForSelectedUtr = currentBalance + selectedUtrExistingPaid; amountController.text = selectedPaid > 0 ? selectedPaid.toStringAsFixed(2) : ''; if (selectedDate != null && selectedDate.toString().trim().isNotEmpty) { _paymentDate = DateTime.tryParse(selectedDate.toString()) ?? _paymentDate; } }); }, ) else TextFormField( readOnly: true, initialValue: generatedUtrNo, decoration: const InputDecoration( labelText: 'Generated UTR Number', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextFormField( controller: amountController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration( labelText: 'Paid Amount', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextFormField( readOnly: true, controller: TextEditingController( text: DateFormat('dd-MM-yyyy').format(_paymentDate), ), decoration: const InputDecoration( labelText: 'Paid Date', border: OutlineInputBorder(), suffixIcon: Icon(Icons.calendar_today_outlined, size: 18), ), onTap: () async { final now = DateTime.now(); final minDate = DateTime(now.year, now.month, now.day); final picked = await showDatePicker( context: ctx, initialDate: _paymentDate.isBefore(minDate) ? minDate : _paymentDate, firstDate: minDate, lastDate: DateTime(2100), ); if (picked != null) { setLocalState(() { _paymentDate = picked; }); } }, ), ] else Text( 'Invoice fully paid. No payment entry required.', style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w500, color: const Color(0xFF2E7D6E), ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel'), ), if (currentBalance > 0) ElevatedButton( onPressed: () async { final enteredAmount = _toDouble(amountController.text.trim()); if (enteredAmount <= 0) { ToastHelper.showWarningToast(context, 'Enter valid paid amount'); return; } if (utrDetails.isNotEmpty && selectedUtrId == null) { ToastHelper.showWarningToast(context, 'Please choose the UTR number'); return; } if (enteredAmount < selectedUtrExistingPaid) { ToastHelper.showWarningToast( context, 'Paid amount cannot be less than existing paid amount', ); return; } final maxAllowed = maxAllowedForSelectedUtr > 0 ? maxAllowedForSelectedUtr : currentBalance; if (enteredAmount > maxAllowed) { ToastHelper.showWarningToast( context, 'Paid amount cannot exceed balance amount', ); return; } Navigator.pop(ctx, true); }, child: const Text('Save'), ), ], ); }, ); }, ); if (result != true) return; try { setState(() => isLoading = true); final response = await apiService.updateInvoiceUtrDetails({ 'invoice_id': int.tryParse(item['id'].toString()) ?? 0, 'paid_amount': _toDouble(amountController.text.trim()), 'utr_id': selectedUtrId != null ? int.tryParse('$selectedUtrId') : null, 'utr_no': selectedUtrId == null ? generatedUtrNo : null, 'paid_date': DateFormat('yyyy-MM-dd').format(_paymentDate), 'created_by': userId, 'updated_by': userId, }); if (response['status'] == 'success') { ToastHelper.showSuccessToast(context, 'UTR details updated successfully'); await getPayoutList(); } else { ToastHelper.showWarningToast( context, response['message']?.toString() ?? 'Failed to update UTR details', ); } } catch (e) { ToastHelper.showErrorToast(context, e.toString()); } finally { setState(() => isLoading = false); } } void _showAccountHistoryModal(Map item) { showDialog( context: context, builder: (_) => AccountHistoryModal( invoiceId: item['id'], ), ); } @override Widget build(BuildContext context) { return MainLayout( title: "Pay Out", body: SelectionArea( child: Container( // height: 30, // color: Colors.yellow.shade50, width: MediaQuery.of(context).size.width, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ // Container( // height: 30, // width: MediaQuery.of(context).size.width, // child: GestureDetector( // onTap: () { // context.go(AppRoutes.dashboard); // }, // 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.dashboard); // // }, // // splashRadius: 18, // // hoverColor: Colors.black12, // // padding: const EdgeInsets.all(4), // // constraints: const BoxConstraints(), // // ), // // ), // // const SizedBox(width: 5), // spacing between icon and text // Text( // "Invoice", // style: GoogleFonts.inter( // fontSize: 14, // fontWeight: FontWeight.w500, // ), // ), // ], // ), // ), // ), // // SizedBox(height: 5), Expanded( child: Container( width: MediaQuery.of(context).size.width, padding: EdgeInsets.all(8.0), child: _buildContent(context), ), ), Container( // height: 20, width: MediaQuery.of(context).size.width, // color: Colors.green.shade50, child: PaginationControls( currentPage: currentPage, itemsPerPage: itemsPerPage, // totalItems: dataVal.length, totalItems: filteredData.length, // activeColor: layoutColor, // your theme color onPageChanged: (page) { setState(() { currentPage = page; }); }, onItemsPerPageChanged: (items) { setState(() { itemsPerPage = items; currentPage = 1; }); }, ), ), ], ), ) ,) , ); } Widget _payoutFilterField({ required String hint, required TextEditingController controller, required double width, }) { return Container( width: width, height: _payoutFilterFieldHeight, padding: const EdgeInsets.symmetric(horizontal: 15), alignment: Alignment.center, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFE2E8F0)), ), child: TextField( controller: controller, style: GoogleFonts.poppins(fontSize: 11, color: Colors.black), decoration: InputDecoration( isDense: true, hintText: hint, hintStyle: GoogleFonts.poppins( fontSize: 11, color: const Color(0xFF64748B), ), border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, contentPadding: EdgeInsets.zero, ), ), ); } Widget _payoutFilterActions() { return Row( mainAxisSize: MainAxisSize.min, children: [ Tooltip( message: 'Filter', child: IconButton( icon: const Icon( Icons.search_rounded, size: 18, color: Color(0xFF94A3B8), ), onPressed: () { if (_formKey.currentState!.validate()) { filterDateRange(); } }, ), ), Tooltip( message: 'Refresh', child: IconButton( onPressed: refrshfilterDateRange, icon: const Icon( Icons.refresh, size: 18, color: Color(0xFF94A3B8), ), ), ), ], ); } Widget _payoutTextFilters(BuildContext context) { final fieldWidth = ResponsiveLayout.isMobile(context) ? 120.0 : 140.0; final isMobile = ResponsiveLayout.isMobile(context); final fields = [ _payoutFilterField( hint: 'Agent Name', controller: controllers['agentName']!, width: fieldWidth, ), _payoutFilterField( hint: 'Agent Code', controller: controllers['agentCode']!, width: fieldWidth, ), _payoutFilterField( hint: 'Policy No', controller: controllers['policyNo']!, width: fieldWidth, ), _payoutFilterActions(), ]; if (isMobile) { return Wrap( spacing: 8, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: fields, ); } return Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ for (var i = 0; i < fields.length; i++) ...[ if (i > 0) const SizedBox(width: 8), fields[i], ], ], ); } Widget _payoutInvoiceDateFilter(BuildContext context) { final isMobile = ResponsiveLayout.isMobile(context); final dateFilter = DateFilterRow( compactDateFiltersOnly: true, hideFilterActions: true, dataFrom: 'Payout', role: roleId, id: userId, selectedStaffId: null, startController: controllers['startDate']!, endController: controllers['endDate']!, onStatusChanged: (_) {}, formKey: _formKey, isMobile: isMobile, onFilter: filterDateRange, onRefresh: refrshfilterDateRange, ); if (isMobile) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ dateFilter, const SizedBox(height: 8), _payoutTextFilters(context), ], ); } return Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ dateFilter, const SizedBox(width: 8), _payoutTextFilters(context), ], ); } Widget _buildContent(BuildContext context) { return Column( children: [ if (ResponsiveLayout.isMobile(context)) Padding( padding: const EdgeInsets.only(bottom: 8), child: _payoutInvoiceDateFilter(context), ), Container( // height: 40, // color: Colors.pink, child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [ Text( "Payout", style: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.w400, ), ), if (!ResponsiveLayout.isMobile(context)) ...[ const SizedBox(width: 16), Expanded( child: Align( alignment: Alignment.centerLeft, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: _payoutInvoiceDateFilter(context), ), ), ), ] else ...[ const Spacer(), ], ThemedSearchField( hintText: 'Search', // backgroundColor: Color(0xFFF6F8F8), onChanged: filterData, controller: _searchStaffController, txtHeight: 30, txtwidth: ResponsiveLayout.isMobile(context) ? MediaQuery.of(context).size.width * 0.7 : MediaQuery.of(context).size.width * 0.15, ), SizedBox(width: 10), Tooltip( message: 'Add Payout Details', child: InkWell( onTap: () async { context.go(AppRoutes.payoutDetails); }, child: Container( padding: const EdgeInsets.all(4.0), decoration: BoxDecoration( color: const Color(0xFF2E7D6E), // color: const Color(0xFF425B5B), borderRadius: BorderRadius.circular(8.0), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ // Image.asset( // "assets/miscellaneous/export.png", // height: 25, // width: 25, // ), Icon(Icons.add, color: Colors.white, size: 18), // Image.asset("assets/miscellaneous/export", height: 15, width: 15), ], ), ), ), ), SizedBox(width: 10), ExportBtn( sheetName: "Payout", fileName: "Payout_List", data: filteredData, txt: true, displayHeaders: [ "S.No", "Invoice No", "Invoice Date", "UTR Number", "Invoiced Amount", "Payout Amount", "Balance Amount", "Status", ], keys: [ "id", "invoice_no", "invoice_date", "utr_numbers", "invoiced_amount", "payout_amount", "balance_amount", "payout_status", ], valueFormatters: { 'payout_status': (v, _) => _exportPayoutStatusLabel(v), }, ), ], ), ), SizedBox(height: 5), Container( decoration: BoxDecoration( color: Color(0xFFF1F5F9), borderRadius: BorderRadius.circular(6), ), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Row( children: [ Expanded(flex: 1, child: Text('S.No', style: _headerStyle)), Expanded( flex: 2, child: Text('Invoice Date', style: _headerStyle), ), Expanded( flex: 3, child: Text('Invoice No', style: _headerStyle), ), Expanded(flex: 2, child: Text('UTR Number', style: _headerStyle)), Expanded(flex: 2, child: Text('Invoiced Amount', style: _headerStyle),), Expanded( flex: 2, child: Text('Payout Amount', style: _headerStyle), ), Expanded( flex: 2, child: Text('Balance Amount', style: _headerStyle), ), Expanded(flex: 2, child: Text('Status', style: _headerStyle)), Expanded(flex: 2, child: Text('Action', style: _headerStyle)), ], ), ), Expanded(child: _buildDataTable(context)), ], ); } Widget _buildDataTable(BuildContext context) { if (filteredData.isEmpty) { return const SizedBox( height: 50, child: Center(child: Text('No available data')), ); } final sortedData = [..._paginatedData]; // Desktop: keep ListView.builder return ListView.builder( itemCount: sortedData.length + 1, itemBuilder: (context, index) { if (index == 0) return _buildHeader(); final startIndex = (currentPage - 1) * itemsPerPage; final item = sortedData[index - 1]; final sno = startIndex + index; return _buildDataRow(item, sno); }, ); } Widget _buildHeader() { return SizedBox.shrink(); } Widget _buildDataRow(Map item, sno) { return Container( padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16), // margin: const EdgeInsets.only(top: 10), decoration: BoxDecoration( color: Colors.white, // color: Color(0xFFE0F7F9), border: const Border( bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1), ), borderRadius: BorderRadius.circular(8), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 1, child: Text('$sno', style: _dataBold), ), Expanded( flex: 2, child: Text( item['invoice_date'] != null ? _formatDate(item['invoice_date']) : '-', style: _dataBold, ), ), Expanded( flex: 3, child: Text( _displayText(item['invoice_no']), style: _dataBold, softWrap: true, maxLines: 3, ), ), Expanded( flex: 2, child: Tooltip( message: _utrTooltipText(item['utr_numbers']), waitDuration: const Duration(milliseconds: 250), child: Text( _truncateText(_displayText(item['utr_numbers']), maxChars: 10), style: _dataBold, softWrap: false, overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ), Expanded( flex: 2, child: Text( _displayAmount(item['invoiced_amount']), style: _dataBold, softWrap: true, maxLines: 3, ), ), Expanded( flex: 2, child: Text( _displayAmount(item['payout_amount']), style: _dataBold, softWrap: true, maxLines: 3, ), ), Expanded( flex: 2, child: Text( _displayAmount(item['balance_amount']), style: _dataBold, softWrap: true, maxLines: 3, ), ), Expanded( flex: 2, child: Text( // item['payout_status'] != null ? item['payout_status'] : '-', item['payout_status'] == '1' ? 'Pending' : item['payout_status'] != null ? 'Completed' : '-', style: _dataBold, softWrap: true, maxLines: 3, ), ), // ACTION ICONS (Edit + Delete) Expanded( flex: 2, child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ // Step 1: Open payout/view screen for selected invoice. IconButton( icon: const Icon( Icons.remove_red_eye_outlined, color: Colors.blue, size: _actionIconSize, ), tooltip: 'View Payout', onPressed: () { context.go(AppRoutes.payoutDetailsView, extra: item); print("PL 9 => View clicked for ${item['invoice_no']}"); }, splashRadius: 16, hoverColor: Colors.black12, padding: EdgeInsets.zero, constraints: const BoxConstraints.tightFor( width: _actionButtonSize, height: _actionButtonSize, ), visualDensity: VisualDensity.compact, ), const SizedBox(width: _actionIconGap), // Step 2: Add payment only when balance amount is pending. // IconButton( // icon: const Icon( // Icons.currency_rupee, // color: Colors.green, // size: _actionIconSize, // ), // tooltip: 'Add payment', // onPressed: () { // final balance = _toDouble(item['balance_amount']); // if (balance <= 0) { // ToastHelper.showWarningToast(context, 'Invoice already fully paid'); // return; // } // _showAddPaymentDialog(item); // }, // splashRadius: 16, // hoverColor: Colors.black12, // padding: EdgeInsets.zero, // constraints: const BoxConstraints.tightFor( // width: _actionButtonSize, // height: _actionButtonSize, // ), // visualDensity: VisualDensity.compact, // ), // const SizedBox(width: _actionIconGap), // Step 3: Show account history modal for this invoice. IconButton( icon: const Icon( Icons.history, color: Colors.green, size: _actionIconSize, ), tooltip: 'Account details', onPressed: () => _showAccountHistoryModal(item), splashRadius: 16, hoverColor: Colors.black12, padding: EdgeInsets.zero, constraints: const BoxConstraints.tightFor( width: _actionButtonSize, height: _actionButtonSize, ), visualDensity: VisualDensity.compact, ), const SizedBox(width: _actionIconGap), // Step 4: Delete invoice entry. IconButton( icon: const Icon(Icons.delete, color: Colors.red, size: _actionIconSize), tooltip: 'Delete Payout', onPressed: () { // TODO: handle delete deleteInvoice(item['id']); print("PL 10 => Delete clicked for ${item['invoice_no']}"); }, splashRadius: 16, hoverColor: Colors.black12, padding: EdgeInsets.zero, constraints: const BoxConstraints.tightFor( width: _actionButtonSize, height: _actionButtonSize, ), visualDensity: VisualDensity.compact, ), ], ), ), ], ), ); } static final _dataBold = GoogleFonts.inter( fontSize: 11.5, fontWeight: FontWeight.w400, color: Color(0xFF000000), ); static final _headerStyle = GoogleFonts.poppins( fontSize: 11.2, fontWeight: FontWeight.w500, color: Color(0xFF1E293B), ); } class AccountHistoryModal extends StatefulWidget { final dynamic invoiceId; const AccountHistoryModal({super.key, required this.invoiceId}); @override State createState() => _AccountHistoryModalState(); } class _AccountHistoryModalState extends State { late ApiService apiService; bool isLoading = true; List> accountHistoryData = []; Map? invoiceSummary; final TextStyle _historyHeaderStyle = GoogleFonts.poppins( fontSize: 11.2, fontWeight: FontWeight.w500, color: const Color(0xFF1E293B), ); final TextStyle _historyCellStyle = GoogleFonts.inter( fontSize: 11.5, fontWeight: FontWeight.w400, color: const Color(0xFF000000), ); String _formatINR(dynamic value) { final amount = double.tryParse(value?.toString() ?? '') ?? 0; return NumberFormat.currency( locale: 'en_IN', symbol: '₹', decimalDigits: 0, ).format(amount); } String _formatTimelineDate(dynamic rawDate) { if (rawDate == null) return '-'; try { final dt = DateTime.parse(rawDate.toString()); return DateFormat('d MMM yyyy').format(dt); } catch (_) { return rawDate.toString(); } } double _toDouble(dynamic value) { if (value == null) return 0; return double.tryParse(value.toString()) ?? 0; } Widget _buildWebActivityBody() { // Step 1: Sort payments by paid date so we can take "latest" balance. final sortedPayments = [...accountHistoryData]..sort((a, b) { DateTime da = DateTime.tryParse((a['paid_date'] ?? '').toString()) ?? DateTime.fromMillisecondsSinceEpoch(0); DateTime db = DateTime.tryParse((b['paid_date'] ?? '').toString()) ?? DateTime.fromMillisecondsSinceEpoch(0); return da.compareTo(db); }); final latestBalance = sortedPayments.isEmpty ? 0.0 : _toDouble(sortedPayments.last['balance_amount']); final paidToDate = sortedPayments.fold( 0.0, (sum, item) => sum + _toDouble(item['paid_amount']), ); final totalAmount = invoiceSummary != null ? _toDouble(invoiceSummary?['total_amount']) : paidToDate + latestBalance; // Step 2: Build UI sections (responsive and cleaner typography). final invoiceNo = _displayText(invoiceSummary?['invoice_no']); final invoiceLabel = invoiceNo.startsWith('#') ? invoiceNo : '#$invoiceNo'; final width = MediaQuery.of(context).size.width; final bool isCompact = width < 1200; final double titleFont = isCompact ? 17 : 20; final double amountBigFont = isCompact ? 22 : 26; final double sectionTitleFont = isCompact ? 12 : 14; final double bodyFont = isCompact ? 11 : 12; final double valueFont = isCompact ? 14 : 16; return Padding( padding: EdgeInsets.all(isCompact ? 10 : 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Step 2a: Top row (Invoice ID only). Row( children: [ Text( invoiceLabel, style: GoogleFonts.inter( fontSize: titleFont, fontWeight: FontWeight.w800, color: const Color(0xFF0F172A), ), ), ], ), const SizedBox(height: 12), // Step 2b: Total amount card. Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), decoration: BoxDecoration( color: const Color(0xFFF8FAFC), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFE5E7EB)), ), child: Row( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'TOTAL AMOUNT', style: GoogleFonts.poppins( fontSize: bodyFont, fontWeight: FontWeight.w600, letterSpacing: 0.2, color: const Color(0xFF334155), ), ), const SizedBox(height: 6), Text( _formatINR(totalAmount), style: GoogleFonts.inter( fontSize: amountBigFont, fontWeight: FontWeight.w900, color: const Color(0xFF0B3A6B), ), ), ], ), const Spacer(), Container( width: 54, height: 54, decoration: BoxDecoration( color: const Color(0xFFEAF2FF), borderRadius: BorderRadius.circular(10), ), child: const Icon( Icons.receipt_long_rounded, color: Color(0xFF2563EB), ), ), ], ), ), const SizedBox(height: 16), // Step 2c: Payment timeline header. Row( children: [ Text( 'PAYMENT TIMELINE', style: GoogleFonts.poppins( fontSize: sectionTitleFont, fontWeight: FontWeight.w700, color: const Color(0xFF111827), ), ), const Spacer(), Text( '${sortedPayments.length} TRANSACTIONS', style: GoogleFonts.inter( fontSize: bodyFont, fontWeight: FontWeight.w600, color: const Color(0xFF334155), ), ), ], ), const SizedBox(height: 10), // Step 2d: Timeline list. Expanded( child: ListView.builder( padding: const EdgeInsets.only(top: 2), itemCount: sortedPayments.length, itemBuilder: (context, index) { // Show latest first on the UI. final data = sortedPayments[sortedPayments.length - 1 - index]; final name = _displayText(data['createdby_name']); final paidAmount = _formatINR(data['paid_amount']); final date = _formatTimelineDate(data['paid_date']); return Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFE5E7EB)), ), child: Row( children: [ Container( width: 40, height: 40, decoration: const BoxDecoration( color: Color(0xFFECFDF5), shape: BoxShape.circle, ), child: const Icon( Icons.check_circle, size: 22, color: Color(0xFF16A34A), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name, style: GoogleFonts.inter( fontSize: isCompact ? 12 : 13, fontWeight: FontWeight.w700, color: const Color(0xFF0F172A), ), ), const SizedBox(height: 4), Text( date, style: GoogleFonts.inter( fontSize: bodyFont, fontWeight: FontWeight.w500, color: const Color(0xFF475569), ), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( paidAmount, style: GoogleFonts.inter( fontSize: isCompact ? 14 : 15, fontWeight: FontWeight.w800, color: const Color(0xFF0B3A6B), ), ), ], ), ], ), ); }, ), ), const SizedBox(height: 12), // Step 2e: Bottom summary. Row( children: [ Expanded( child: Text( 'TOTAL AMOUNT', style: GoogleFonts.poppins( fontSize: bodyFont, fontWeight: FontWeight.w700, color: const Color(0xFF334155), ), ), ), Text( _formatINR(totalAmount), style: GoogleFonts.inter( fontSize: valueFont, fontWeight: FontWeight.w900, color: const Color(0xFF0F172A), ), ), ], ), const SizedBox(height: 8), Row( children: [ Expanded( child: Text( 'PAID TO DATE', style: GoogleFonts.poppins( fontSize: bodyFont, fontWeight: FontWeight.w700, color: const Color(0xFF334155), ), ), ), Text( _formatINR(paidToDate), style: GoogleFonts.inter( fontSize: valueFont, fontWeight: FontWeight.w900, color: const Color(0xFF0F172A), ), ), ], ), const SizedBox(height: 10), Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), decoration: BoxDecoration( color: const Color(0xFFBFDBFE).withOpacity(0.45), borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Expanded( child: Text( 'BALANCE', style: GoogleFonts.poppins( fontSize: bodyFont, fontWeight: FontWeight.w800, color: const Color(0xFF0F172A), ), ), ), Text( _formatINR(latestBalance), style: GoogleFonts.inter( fontSize: isCompact ? 16 : 18, fontWeight: FontWeight.w900, color: const Color(0xFF0B3A6B), ), ), ], ), ), ], ), ); } @override void initState() { super.initState(); apiService = ApiService(); fetchAccountHistory(); } String _displayText(dynamic value) { if (value == null) return '-'; final text = value.toString().trim(); if (text.isEmpty || text.toLowerCase() == 'null') return '-'; return text; } String _displayAmount(dynamic value) { if (value == null) return '0.00'; final parsed = double.tryParse(value.toString()) ?? 0; return parsed.toStringAsFixed(2); } String _formatIndianDate(dynamic rawDate) { if (rawDate == null) return '-'; try { final dateTime = DateTime.parse(rawDate.toString()); return DateFormat('dd-MM-yyyy hh:mm a').format(dateTime); } catch (_) { return rawDate.toString(); } } DateTime _parseDateTime(dynamic rawDate) { if (rawDate == null) { return DateTime.fromMillisecondsSinceEpoch(0); } return DateTime.tryParse(rawDate.toString()) ?? DateTime.fromMillisecondsSinceEpoch(0); } DateTime _rowEffectiveDate(Map row) { final paidAt = _parseDateTime(row['paid_date']); if (paidAt.millisecondsSinceEpoch > 0) return paidAt; final createdAt = _parseDateTime(row['created_at']); if (createdAt.millisecondsSinceEpoch > 0) return createdAt; return _parseDateTime(row['updated_at']); } String _formatPaidDateTime(Map row) { final paidAt = _parseDateTime(row['paid_date']); if (paidAt.millisecondsSinceEpoch > 0 && paidAt.hour + paidAt.minute > 0) { return DateFormat('dd-MM-yyyy hh:mm a').format(paidAt); } final createdAt = _parseDateTime(row['created_at']); if (createdAt.millisecondsSinceEpoch > 0) { return DateFormat('dd-MM-yyyy hh:mm a').format(createdAt); } if (paidAt.millisecondsSinceEpoch > 0) { return DateFormat('dd-MM-yyyy').format(paidAt); } return '-'; } List> _buildMeaningfulHistoryRows() { final sortedRows = [...accountHistoryData]..sort((a, b) { final dateCompare = _rowEffectiveDate(a).compareTo(_rowEffectiveDate(b)); if (dateCompare != 0) return dateCompare; final idA = int.tryParse('${a['id'] ?? 0}') ?? 0; final idB = int.tryParse('${b['id'] ?? 0}') ?? 0; return idA.compareTo(idB); }); double runningPaid = 0; final rowsWithMetrics = sortedRows.map((row) { final paid = _toDouble(row['paid_amount']); runningPaid += paid; return { ...row, 'total_paid_till_date': runningPaid, }; }).toList(); return rowsWithMetrics.reversed.toList(); } Future fetchAccountHistory() async { setState(() => isLoading = true); try { final response = await apiService.listInvoiceUtrDetails(widget.invoiceId); if (response['status'] == 'success') { setState(() { accountHistoryData = List>.from( response['data'] ?? [], ); invoiceSummary = response['invoice'] is Map ? Map.from(response['invoice']) : null; }); } else { setState(() { accountHistoryData = []; invoiceSummary = null; }); } } catch (_) { setState(() { accountHistoryData = []; invoiceSummary = null; }); } finally { if (mounted) setState(() => isLoading = false); } } String _safeFileNamePart(String value) { return value.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim(); } Future _exportAccountHistoryExcel() async { if (accountHistoryData.isEmpty) { ToastHelper.showWarningToast(context, 'No account details data to export'); return; } final invoiceNo = _displayText(invoiceSummary?['invoice_no']); final cleanedInvoiceNo = _safeFileNamePart( invoiceNo == '-' ? 'Invoice number' : invoiceNo, ); final exportRows = _buildMeaningfulHistoryRows(); // ExcelExporter internally reverses incoming rows before writing. // So we pass oldest->latest to get latest->oldest in the final file. final exportInputRows = exportRows.reversed.toList(); final exportData = exportInputRows.asMap().entries.map((entry) { final index = entry.key; final row = entry.value; return { 's_no': index + 1, 'invoice_no': _displayText(row['invoice_no']), 'utr_no': _displayText(row['utr_no']), 'total_amount': _displayAmount(row['total_amount']), 'paid_date_time': _formatPaidDateTime(row), 'createdby_name': _displayText(row['createdby_name']), 'paid_amount': _displayAmount(row['paid_amount']), 'total_paid_till_date': _displayAmount(row['total_paid_till_date']), 'balance_amount': _displayAmount(row['balance_amount']), }; }).toList(); await ExcelExporter.exportToExcel( sheetName: 'Account Details', fileName: 'Account details $cleanedInvoiceNo', data: exportData, displayHeaders: const [ 'S.No', 'Invoice Number', 'UTR Number', 'Total Amount', 'Paid Date & Time', 'Updated By', 'Paid Amount', 'Total Paid Till Date', 'Balance', ], keys: const [ 's_no', 'invoice_no', 'utr_no', 'total_amount', 'paid_date_time', 'createdby_name', 'paid_amount', 'total_paid_till_date', 'balance_amount', ], ); } @override Widget build(BuildContext context) { final bool isMobile = ResponsiveLayout.isMobile(context); final media = MediaQuery.of(context).size; return Dialog( insetPadding: EdgeInsets.symmetric( horizontal: isMobile ? 12 : 24, vertical: isMobile ? 18 : 24, ), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), clipBehavior: Clip.antiAlias, child: ConstrainedBox( constraints: BoxConstraints( maxWidth: isMobile ? media.width : 1100, maxHeight: media.height * (isMobile ? 0.86 : 0.82), ), child: Column( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: const BoxDecoration( color: Color(0xFFF8FAFC), borderRadius: BorderRadius.only( topLeft: Radius.circular(14), topRight: Radius.circular(14), ), border: Border(bottom: BorderSide(color: Color(0xFFE5E7EB))), ), child: Row( children: [ Text( 'Account Details', style: GoogleFonts.poppins( fontSize: 14, color: const Color(0xFF2E7D6E), fontWeight: FontWeight.w600, ), ), const Spacer(), TextButton.icon( onPressed: isLoading ? null : _exportAccountHistoryExcel, icon: const Icon(Icons.file_download_outlined, size: 18), label: const Text('Export Excel'), style: TextButton.styleFrom( foregroundColor: const Color(0xFF2E7D6E), ), ), IconButton( onPressed: () => Navigator.pop(context), icon: const Icon( Icons.close, size: 18, color: Color(0xFF2E7D6E), ), splashRadius: 18, ), ], ), ), Expanded( child: isLoading ? const Center(child: CircularProgressIndicator()) : accountHistoryData.isEmpty ? const Center(child: Text('No Account Details Data Found')) : Padding( padding: const EdgeInsets.all(12), child: Container( color: Colors.white, child: LayoutBuilder( builder: (context, constraints) { final historyRows = _buildMeaningfulHistoryRows(); return Scrollbar( thumbVisibility: true, child: SingleChildScrollView( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: ConstrainedBox( constraints: BoxConstraints( minWidth: constraints.maxWidth, ), child: DataTable( headingRowColor: WidgetStateProperty.all( const Color(0xFFF1F5F9), ), dataRowMinHeight: 42, dataRowMaxHeight: 52, columnSpacing: isMobile ? 14 : 20, columns: [ DataColumn( label: Text('S.No', style: _historyHeaderStyle), ), DataColumn( label: Text( 'Invoice Number', style: _historyHeaderStyle, ), ), DataColumn( label: Text( 'Total Amount', style: _historyHeaderStyle, ), ), DataColumn( label: Text( 'UTR Number', style: _historyHeaderStyle, ), ), DataColumn( label: Text( 'Paid Date & Time', style: _historyHeaderStyle, ), ), DataColumn( label: Text('Updated By', style: _historyHeaderStyle), ), DataColumn( label: Text( 'Paid Amount', style: _historyHeaderStyle, ), ), DataColumn( label: Text( 'Total Paid Till Date', style: _historyHeaderStyle, ), ), DataColumn( label: Text('Balance', style: _historyHeaderStyle), ), ], rows: List.generate(historyRows.length, (index) { final data = historyRows[index]; return DataRow( cells: [ DataCell( Text('${index + 1}', style: _historyCellStyle), ), DataCell( Text( _displayText(data['invoice_no']), style: _historyCellStyle, ), ), DataCell( Text( _displayAmount(data['total_amount']), style: _historyCellStyle, ), ), DataCell( Text( _displayText(data['utr_no']), style: _historyCellStyle, ), ), DataCell( Text( _formatPaidDateTime(data), style: _historyCellStyle, ), ), DataCell( Text( _displayText(data['createdby_name']), style: _historyCellStyle, ), ), DataCell( Text( _displayAmount(data['paid_amount']), style: _historyCellStyle, ), ), DataCell( Text( _displayAmount(data['total_paid_till_date']), style: _historyCellStyle, ), ), DataCell( Text( _displayAmount(data['balance_amount']), style: _historyCellStyle, ), ), ], ); }), ), ), ), ), ); }, ), ), ), ), ], ), ), ); } }