import 'package:dropdown_search/dropdown_search.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:intl/intl.dart'; import '../../../core/services/api_service.dart'; import '../../../data/utils/Pagination.dart'; import '../../../data/utils/toastNotification.dart'; import '../../layouts/main_layout.dart'; import '../../layouts/responsive_layout.dart'; import '../../providers/manager_provider.dart'; import '../../providers/userRoleProvider.dart'; import '../../themes/indicators/date_range_picker_field.dart'; import '../../themes/indicators/search_field_theme.dart'; /// Payout report — layout and styling aligned with [policylist] policy list screen. class PayoutReportScreen extends ConsumerStatefulWidget { const PayoutReportScreen({super.key}); @override ConsumerState createState() => _PayoutReportScreenState(); } class _PayoutReportScreenState extends ConsumerState { late ApiService apiService; int currentPage = 1; int itemsPerPage = 10; bool isLoading = false; final TextEditingController _startController = TextEditingController(); final TextEditingController _endController = TextEditingController(); final TextEditingController _searchController = TextEditingController(); List> _rows = []; List> _filtered = []; List> _agentOptions = []; String? _selectedAgentId; /// `null` = "Select Payout Raised" (not chosen). `'all'` | `'yes'` | `'no'` sent to API. String? _payoutRaised; bool _hasSearched = false; /// From API root `count` and `summary.{total,raised,pending}`. int? _reportCount; num? _summaryTotal; num? _summaryRaised; num? _summaryPending; int? _managerId; bool _isAgentRole() { final r = (ref.read(userRoleProvider) ?? '').toString().trim().toLowerCase(); return r == 'agent' || r == 'a'; } /// `received_or_not` equals `no` (case-insensitive). bool _receivedIsNo(Map item) { final v = (item['received_or_not'] ?? '').toString().trim().toLowerCase(); return v == 'no'; } /// UI / export: capitalize first letter, lowercase the rest (e.g. `yes` → `Yes`). String _receivedLabel(dynamic v) { final s = v?.toString().trim() ?? ''; if (s.isEmpty) return '-'; return s[0].toUpperCase() + (s.length > 1 ? s.substring(1).toLowerCase() : ''); } bool _isAccountsReceivedRole() { final role = (ref.read(userRoleProvider) ?? '') .toString() .trim() .toLowerCase() .replaceAll(RegExp(r'[\s_\-]+'), ''); return role == 'accountsreceived' || role == 'accounts'; } String _receivedDisplayLabel(dynamic v) { final raw = v?.toString().trim().toLowerCase() ?? ''; if (_isAccountsReceivedRole()) { if (raw == 'yes' || raw == 'received' || raw == 'paid') { return 'Paid'; } } return _receivedLabel(v); } String? _agentIdForApi() { if (_isAgentRole()) { final uid = ref.read(userIdProvider); return uid?.toString(); } return _selectedAgentId; } static const _tableHeaderBg = Color(0xFFF1F5F9); static const _rowBorder = Color(0xFFEAEAEA); /// Pending summary chip + `received_or_not == no` row highlight (same palette). static const _pendingChipBg = Color(0xFFFFEBEE); static const _pendingChipBorder = Color(0xFFEF9A9A); static const _pendingChipValue = Color(0xFFC62828); static const _filterStripMobileBg = Color(0xffD9EBE8); /// Closed filter fields (light outline, white fill) — first design. static const _filterFieldBorder = Color(0xFFE2E8F0); static const _filterFocusTeal = Color(0xFF50A398); static final _dataBold = GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w400, color: const Color(0xFF000000), ); /// Table column titles (policy list: bold header row). static final _headerColumn = GoogleFonts.poppins( fontSize: 11.2, fontWeight: FontWeight.w600, color: const Color(0xFF1E293B), ); InputDecoration _filterDropdownDecoration({required String hintText}) { return InputDecoration( isDense: true, hintText: hintText, hintStyle: GoogleFonts.inter(fontSize: 11, color: const Color(0xFF94A3B8)), filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: _filterFieldBorder, width: 1), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: _filterFieldBorder, width: 1), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: _filterFocusTeal, width: 1.5), ), ); } /// `agent/agentList` exposes display name as `name`; older payloads may use `agent_name`. String _partnerDisplayName(Map m) { final v = m['name'] ?? m['agent_name'] ?? m['partner_name'] ?? ''; return v.toString().trim(); } Map _allAgentItem() => { 'id': '', 'name': 'Select Agent', 'agent_name': 'Select Agent', 'agent_code': '', }; Map _selectedAgentMap() { if (_selectedAgentId == null || _selectedAgentId!.isEmpty) { return _allAgentItem(); } try { final m = _agentOptions.firstWhere( (a) => a['id'].toString() == _selectedAgentId, ); return Map.from(m); } catch (_) { return _allAgentItem(); } } List> _payoutRaisedChoices() => [ {'v': null, 'label': 'Select Payout Raised'}, {'v': 'all', 'label': 'All'}, {'v': 'yes', 'label': 'Yes'}, {'v': 'no', 'label': 'No'}, ]; Map _selectedPayoutRaisedMap() { for (final e in _payoutRaisedChoices()) { if (e['v'] == _payoutRaised) return e; } return _payoutRaisedChoices().first; } Widget _buildAgentDropdownSearch(double width) { final agents = _agentOptions .where((a) => (a['id']?.toString() ?? '').isNotEmpty) .map((a) => Map.from(a)) .toList(); return SizedBox( height: 35, width: width, child: DropdownSearch>( selectedItem: _selectedAgentMap(), items: (filter, infiniteScrollProps) { return [_allAgentItem(), ...agents]; }, itemAsString: (m) { if ((m['id']?.toString() ?? '').isEmpty) { return 'Select Agent'; } final name = _partnerDisplayName(m); final code = (m['agent_code'] ?? '').toString(); return '$name $code'.trim(); }, compareFn: (a, b) => (a['id']?.toString() ?? '') == (b['id']?.toString() ?? ''), decoratorProps: DropDownDecoratorProps( decoration: _filterDropdownDecoration(hintText: 'Select Agent'), ), dropdownBuilder: (context, selectedItem) { final m = selectedItem ?? _allAgentItem(); final isAll = (m['id']?.toString() ?? '').isEmpty; final name = _partnerDisplayName(m); final code = (m['agent_code'] ?? '').toString(); final text = isAll ? 'Select Agent' : name.isEmpty ? code : (code.isNotEmpty ? '$name — $code' : name); return Align( alignment: Alignment.centerLeft, child: Text( text, style: GoogleFonts.inter( fontSize: 11, color: isAll ? const Color(0xFF94A3B8) : const Color(0xFF0F172A), ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ); }, popupProps: PopupProps.menu( showSearchBox: true, fit: FlexFit.loose, constraints: const BoxConstraints(maxHeight: 280), menuProps: MenuProps( backgroundColor: Colors.white, elevation: 8, borderRadius: BorderRadius.circular(8), ), searchFieldProps: TextFieldProps( decoration: InputDecoration( filled: true, fillColor: Colors.white, hintText: 'Select Agent', hintStyle: GoogleFonts.inter(fontSize: 12, color: const Color(0xFF94A3B8)), isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: _filterFieldBorder), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: _filterFieldBorder), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: _filterFocusTeal, width: 1.5), ), ), ), itemBuilder: (context, item, isDisabled, isSelected) { final isAll = (item['id']?.toString() ?? '').isEmpty; return Container( color: isSelected ? const Color(0xFFF1F5F9) : null, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: isAll ? Text( 'Select Agent', style: GoogleFonts.inter( fontSize: 12, color: const Color(0xFF64748B), ), ) : Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( () { final n = _partnerDisplayName(item); final c = (item['agent_code'] ?? '').toString(); return (n.isEmpty ? c : n).toUpperCase(); }(), style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w700, color: const Color(0xFF0F172A), ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( (item['agent_code'] ?? '').toString(), style: GoogleFonts.inter( fontSize: 10, fontWeight: FontWeight.w400, color: const Color(0xFF94A3B8), ), ), ], ), ), ); }, ), onChanged: (val) { setState(() { final id = val?['id']?.toString() ?? ''; _selectedAgentId = id.isEmpty ? null : id; }); }, ), ); } Widget _buildPayoutRaisedDropdownSearch(double width) { final choices = _payoutRaisedChoices(); return SizedBox( height: 35, width: width, child: DropdownSearch>( selectedItem: _selectedPayoutRaisedMap(), items: (filter, infiniteScrollProps) => choices, itemAsString: (m) => (m['label'] ?? '').toString(), compareFn: (a, b) => a['v'] == b['v'], decoratorProps: DropDownDecoratorProps( decoration: _filterDropdownDecoration(hintText: 'Select Payout Raised'), ), dropdownBuilder: (context, selectedItem) { final m = selectedItem ?? _payoutRaisedChoices().first; final label = (m['label'] ?? '').toString(); final isUnset = m['v'] == null; return Align( alignment: Alignment.centerLeft, child: Text( label, style: GoogleFonts.inter( fontSize: 11, color: isUnset ? const Color(0xFF94A3B8) : const Color(0xFF0F172A), ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ); }, popupProps: PopupProps.menu( showSearchBox: true, fit: FlexFit.loose, constraints: const BoxConstraints(maxHeight: 220), menuProps: MenuProps( backgroundColor: Colors.white, elevation: 8, borderRadius: BorderRadius.circular(8), ), searchFieldProps: TextFieldProps( decoration: InputDecoration( filled: true, fillColor: Colors.white, hintText: 'Select Payout Raised', hintStyle: GoogleFonts.inter(fontSize: 12, color: const Color(0xFF94A3B8)), isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: _filterFieldBorder), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: _filterFieldBorder), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: _filterFocusTeal, width: 1.5), ), ), ), itemBuilder: (context, item, isDisabled, isSelected) { final isUnset = item['v'] == null; return Container( color: isSelected ? const Color(0xFFF1F5F9) : null, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Text( (item['label'] ?? '').toString(), style: GoogleFonts.inter( fontSize: 12, fontWeight: isUnset ? FontWeight.w400 : FontWeight.w500, color: isUnset ? const Color(0xFF64748B) : const Color(0xFF0F172A), ), ), ); }, ), onChanged: (val) { setState(() { if (val == null) { _payoutRaised = null; } else { _payoutRaised = val['v'] as String?; } }); }, ), ); } @override void initState() { super.initState(); apiService = ApiService(); Future.microtask(() async { _managerId = ref.read(managerIdProvider); if (_managerId != null && !_isAgentRole()) { await _loadAgents(); } }); } @override void dispose() { _startController.dispose(); _endController.dispose(); _searchController.dispose(); super.dispose(); } Future _loadAgents() async { final mid = _managerId; if (mid == null) return; try { final response = await apiService.fetchAgentUserList(mid); if (response['status'] == 'success' && response['data'] is List) { if (mounted) { setState(() { _agentOptions = List>.from(response['data']); }); } } } catch (_) {} } String? _dateToIso(String ddMmYyyy) { final t = ddMmYyyy.trim(); if (t.isEmpty) return null; try { final d = DateFormat('dd-MM-yyyy').parseStrict(t); return DateFormat('yyyy-MM-dd').format(d); } catch (_) { return null; } } bool _validateFiltersForSearch() { final from = _dateToIso(_startController.text); final to = _dateToIso(_endController.text); final startText = _startController.text.trim(); final endText = _endController.text.trim(); if (startText.isNotEmpty || endText.isNotEmpty) { if (from == null || to == null) { ToastHelper.showWarningToast( context, 'Enter both From date and To date, or clear both.', ); return false; } } final hasDates = from != null && to != null; final hasAgent = _isAgentRole() ? ref.read(userIdProvider) != null : (_selectedAgentId != null && _selectedAgentId!.trim().isNotEmpty); final hasPayoutRaised = _payoutRaised != null; if (!hasDates && !hasAgent && !hasPayoutRaised) { ToastHelper.showWarningToast( context, _isAgentRole() ? 'Select at least one filter: date range or payout raised.' : 'Select at least one filter: date range, agent, or payout raised.', ); return false; } return true; } Future _loadReport() async { if (!_validateFiltersForSearch()) return; setState(() { isLoading = true; _hasSearched = true; }); try { final response = await apiService.fetchPayoutReportList( fromDate: _dateToIso(_startController.text), toDate: _dateToIso(_endController.text), agentId: _agentIdForApi(), payoutRaised: _payoutRaised, ); if (!mounted) return; final data = response['data']; final ok = response['status']?.toString().toLowerCase() == 'success' || response['code'] == 200 || data is List; if (ok) { List> next = []; if (data is List) { next = data.map((e) { if (e is Map) return e; if (e is Map) return Map.from(e); return {}; }).toList(); } else if (data is Map) { next = [Map.from(data)]; } int? count; final cr = response['count']; if (cr is int) { count = cr; } else if (cr != null) { count = int.tryParse(cr.toString()); } num? sumTotal; num? sumRaised; num? sumPending; final sm = response['summary']; if (sm is Map) { final m = Map.from(sm); sumTotal = _parseNum(m['total']); sumRaised = _parseNum(m['raised']); sumPending = _parseNum(m['pending']); } setState(() { _rows = next; _reportCount = count; _summaryTotal = sumTotal; _summaryRaised = sumRaised; _summaryPending = sumPending; _applySearch(_searchController.text); currentPage = 1; }); } else { setState(() { _rows = []; _filtered = []; _reportCount = null; _summaryTotal = null; _summaryRaised = null; _summaryPending = null; }); final msg = response['message']?.toString(); if (msg != null && msg.isNotEmpty) { ToastHelper.showWarningToast(context, msg); } } } catch (e) { if (mounted) { ToastHelper.showErrorToast(context, e.toString()); } } finally { if (mounted) setState(() => isLoading = false); } } void _filterData(String query) { setState(() { _applySearch(query); currentPage = 1; }); } void _applySearch(String query) { final q = query.trim().toLowerCase(); if (q.isEmpty) { _filtered = List.from(_rows); return; } _filtered = _rows.where((row) { return row.values.any( (v) => v != null && v.toString().toLowerCase().contains(q), ); }).toList(); } List> get _paginated { if (_filtered.isEmpty) return []; final maxPage = (_filtered.length / itemsPerPage).ceil().clamp(1, 999999); final safe = currentPage.clamp(1, maxPage); final start = (safe - 1) * itemsPerPage; final end = (start + itemsPerPage).clamp(0, _filtered.length); return _filtered.sublist(start, end); } String _cell(Map row, List keys) { for (final k in keys) { final v = row[k]; if (v != null && v.toString().trim().isNotEmpty) { return v.toString(); } } return '-'; } String? _firstNonEmptyRaw(Map row, List keys) { for (final k in keys) { final v = row[k]; if (v != null && v.toString().trim().isNotEmpty) { return v.toString(); } } return null; } /// API sometimes returns payout + UTR in one string (e.g. `5000.00UTR20260401164749`). /// Normalizes into separate display values for PAYOUT and UTR NO columns. ({String payout, String utr}) _payoutAndUtr(Map row) { const utrKeys = ['utr_no', 'utr', 'utr_number']; const payoutKeys = ['commission_amount', 'payout', 'commission']; String utrStr = (_firstNonEmptyRaw(row, utrKeys) ?? '').trim(); String payoutRaw = (_firstNonEmptyRaw(row, payoutKeys) ?? '').trim(); String stripTrailingPlaceholder(String s) => s.replaceAll(RegExp(r'[\s\-–—]+$'), '').trim(); if (payoutRaw.isNotEmpty) { final upper = payoutRaw.toUpperCase(); final idx = upper.indexOf('UTR'); if (idx > 0) { final gluedUtr = payoutRaw.substring(idx).trim(); payoutRaw = stripTrailingPlaceholder(payoutRaw.substring(0, idx)); if (utrStr.isEmpty || utrStr == '-') { utrStr = gluedUtr; } } else { payoutRaw = stripTrailingPlaceholder(payoutRaw); } } if (payoutRaw.isEmpty) payoutRaw = '-'; if (utrStr.isEmpty) utrStr = '-'; return (payout: payoutRaw, utr: utrStr); } static num? _parseNum(dynamic v) { if (v == null) return null; if (v is num) return v; return num.tryParse(v.toString()); } String _fmtSummaryAmount(num? v) { if (v == null) return '-'; return NumberFormat('#,##0.00').format(v); } Widget _buildSummaryChips() { if (!_hasSearched || isLoading) return const SizedBox.shrink(); // API `data: []` — no rows; hide summary even if count/summary are zeros. if (_rows.isEmpty) return const SizedBox.shrink(); final hasAny = _reportCount != null || _summaryTotal != null || _summaryRaised != null || _summaryPending != null; if (!hasAny) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.only(bottom: 8), child: Wrap( spacing: 10, runSpacing: 8, children: [ _summaryChip( label: 'Count', value: _reportCount != null ? '$_reportCount' : '-', bg: const Color(0xFFE8F5E9), border: const Color(0xFFBBDCB5), labelColor: const Color(0xFF64748B), valueColor: const Color(0xFF2E7D32), ), _summaryChip( label: 'Total', value: _fmtSummaryAmount(_summaryTotal), bg: const Color(0xFFECEFF1), border: const Color(0xFFB0BEC5), labelColor: const Color(0xFF64748B), valueColor: const Color(0xFF37474F), ), _summaryChip( label: 'Paid', value: _fmtSummaryAmount(_summaryRaised), bg: const Color(0xFFE0F2F1), border: const Color(0xFF80CBC4), labelColor: const Color(0xFF64748B), valueColor: const Color(0xFF00695C), ), _summaryChip( label: 'Pending', value: _fmtSummaryAmount(_summaryPending), bg: _pendingChipBg, border: _pendingChipBorder, labelColor: const Color(0xFF64748B), valueColor: _pendingChipValue, ), ], ), ); } Widget _summaryChip({ required String label, required String value, required Color bg, required Color border, required Color labelColor, required Color valueColor, }) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(8), border: Border.all(color: border, width: 1), ), child: RichText( text: TextSpan( style: GoogleFonts.inter(fontSize: 12, color: labelColor), children: [ TextSpan(text: '$label: '), TextSpan( text: value, style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w700, color: valueColor, ), ), ], ), ), ); } Future _onExport() async { if (!_validateFiltersForSearch()) return; try { await apiService.downloadCommissionPayoutReportExport( fromDate: _dateToIso(_startController.text), toDate: _dateToIso(_endController.text), agentId: _agentIdForApi(), payoutRaised: _payoutRaised, ); } catch (e) { if (mounted) { ToastHelper.showErrorToast(context, 'Export failed: $e'); } } } Widget _filterControlsRow(BuildContext context) { final spacing = 5.0; final isMobile = ResponsiveLayout.isMobile(context); final role = (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase(); final showAgentFilter = role != 'agent' && role != 'a'; final fields = Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ DateRangePickerField( startController: _startController, endController: _endController, hintText: 'Select date range', showLabelAboveField: false, txtwidth: isMobile ? null : MediaQuery.of(context).size.width * 0.15, lastDate: DateTime.now(), useFormField: false, ), if (showAgentFilter) ...[ SizedBox(width: spacing), _buildAgentDropdownSearch( isMobile ? 168.0 : MediaQuery.of(context).size.width * 0.13, ), ], SizedBox(width: spacing), _buildPayoutRaisedDropdownSearch( isMobile ? 168.0 : MediaQuery.of(context).size.width * 0.13, ), SizedBox(width: spacing), Tooltip( message: 'Search', child: IconButton( icon: const Icon(Icons.search_rounded, size: 18, color: Color(0xFF94A3B8)), onPressed: _loadReport, ), ), Tooltip( message: 'Refresh', child: IconButton( icon: const Icon(Icons.refresh, size: 18, color: Color(0xFF94A3B8)), onPressed: () { _startController.clear(); _endController.clear(); _searchController.clear(); setState(() { _selectedAgentId = null; _payoutRaised = null; _rows = []; _filtered = []; _hasSearched = false; _reportCount = null; _summaryTotal = null; _summaryRaised = null; _summaryPending = null; currentPage = 1; }); }, ), ), ], ); return Container( padding: const EdgeInsets.all(8.0), color: isMobile ? _filterStripMobileBg : null, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: fields, ), ); } Widget _buildSearchAndActions(BuildContext context) { final isMobile = ResponsiveLayout.isMobile(context); final searchField = ThemedSearchField( hintText: 'Search', onChanged: _filterData, controller: _searchController, backgroundColor: const Color(0xFFFFFFFF), txtHeight: 30, txtwidth: isMobile ? MediaQuery.of(context).size.width * 0.7 : MediaQuery.of(context).size.width * 0.15, ); final actions = [ Tooltip( message: 'Export Excel', child: InkWell( onTap: _onExport, child: Container( padding: const EdgeInsets.all(7.0), decoration: BoxDecoration( color: const Color(0xFF2E7D6E), borderRadius: BorderRadius.circular(8.0), ), child: Image.asset( 'assets/miscellaneous/export.png', height: 13, width: 13, ), ), ), ), const SizedBox(width: 8), ]; // Desktop: no Spacer — this Row sits after Expanded(filter); Row+Spacer has // unbounded intrinsic width and triggers RenderFlex layout failure. if (!isMobile) { return Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ searchField, const SizedBox(width: 10), ...actions, ], ); } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ searchField, const Spacer(), ...actions, ], ); } Widget _buildFilterBar(BuildContext context) { final isMobile = ResponsiveLayout.isMobile(context); if (isMobile) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _filterControlsRow(context), const SizedBox(height: 8), _buildSearchAndActions(context), ], ); } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: _filterControlsRow(context)), _buildSearchAndActions(context), ], ); } Widget _tableHeader() { return Container( decoration: BoxDecoration( color: _tableHeaderBg, borderRadius: BorderRadius.circular(6), ), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Row( children: [ Expanded(flex: 1, child: Text('S NO', style: _headerColumn)), Expanded(flex: 2, child: Text('POLICY NO', style: _headerColumn)), Expanded(flex: 2, child: Text('REG NO', style: _headerColumn)), Expanded(flex: 1, child: Text('WEIGHT', style: _headerColumn)), Expanded(flex: 2, child: Text('FUEL TYPE', style: _headerColumn)), Expanded(flex: 2, child: Text('VEHICLE TYPE', style: _headerColumn)), Expanded(flex: 2, child: Text('UTR NO', style: _headerColumn)), Expanded( flex: 2, child: Text('PREMIUM', style: _headerColumn, textAlign: TextAlign.right), ), Expanded( flex: 2, child: Text('PAYOUT', style: _headerColumn, textAlign: TextAlign.right), ), const SizedBox(width: 15), Expanded( flex: 2, child: Text( _isAccountsReceivedRole() ? 'PAID' : 'RECEIVED', style: _headerColumn, ), ), ], ), ); } Widget _dataRow(Map item, int sno) { final highlightNo = _receivedIsNo(item); // Fresh style (not copyWith on _dataBold): GoogleFonts may set `foreground`, // which overrides `color` and can make text invisible on web. final rowStyle = highlightNo ? GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w400, color: const Color(0xFF0F172A), ) : _dataBold; final pu = _payoutAndUtr(item); final row = Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(flex: 1, child: Text('$sno', style: rowStyle)), Expanded( flex: 2, child: Text( _cell(item, ['policy_no']), style: rowStyle, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Expanded( flex: 2, child: Text( _cell(item, ['reg_no']), style: rowStyle, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Expanded( flex: 1, child: Text( _cell(item, ['weight']), style: rowStyle, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Expanded( flex: 2, child: Text( _cell(item, ['fuel_type']), style: rowStyle, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Expanded( flex: 2, child: Text( _cell(item, ['vehicle_type']), style: rowStyle, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Expanded( flex: 2, child: Text( pu.utr, style: rowStyle, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Expanded( flex: 2, child: Text( _cell(item, ['premium']), style: rowStyle, textAlign: TextAlign.right, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Expanded( flex: 2, child: Text( pu.payout, style: rowStyle, textAlign: TextAlign.right, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 15), Expanded( flex: 2, child: Text( _receivedDisplayLabel(item['received_or_not']), style: rowStyle, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ); final padded = Padding( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), child: row, ); if (highlightNo) { // Background in a separate layer so ListView/Container decoration never clips text (web). return Stack( clipBehavior: Clip.none, children: [ Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( color: _pendingChipBg, borderRadius: BorderRadius.circular(8), border: Border( bottom: const BorderSide(color: _rowBorder, width: 1), left: const BorderSide(color: _pendingChipBorder, width: 3), ), ), ), ), padded, ], ); } return Container( clipBehavior: Clip.none, decoration: BoxDecoration( color: Colors.white, border: const Border( bottom: BorderSide(color: _rowBorder, width: 1), ), borderRadius: BorderRadius.circular(8), ), child: padded, ); } Widget _buildDataTable(BuildContext context) { if (!_hasSearched) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( () { final r = (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase(); final hideAgent = r == 'agent' || r == 'a'; return hideAgent ? 'Choose filters (date range and/or payout raised) and tap Search to load the report.' : 'Choose filters (date range, agent, and/or payout raised) and tap Search to load the report.'; }(), textAlign: TextAlign.center, style: GoogleFonts.inter( fontSize: 13, color: const Color(0xFF64748B), height: 1.4, ), ), ), ); } if (isLoading) { return const Center(child: CircularProgressIndicator()); } final pageItems = _paginated; final startIndex = _filtered.isEmpty ? 0 : (currentPage - 1) * itemsPerPage; if (pageItems.isEmpty) { return const SizedBox( height: 50, child: Center(child: Text('No available data')), ); } final isMobile = ResponsiveLayout.isMobile(context); final tableWidth = MediaQuery.of(context).size.width < 1500 ? 1500.0 : MediaQuery.of(context).size.width; if (isMobile) { return Scrollbar( thumbVisibility: true, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox( width: tableWidth, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _tableHeader(), ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: pageItems.length, itemBuilder: (context, i) { return _dataRow(pageItems[i], startIndex + i + 1); }, ), ], ), ), ), ); } // One ListView inside a fixed-height box (no Column+Expanded) avoids relayout // conflicts with horizontal SingleChildScrollView on web. return LayoutBuilder( builder: (context, constraints) { final w = constraints.maxWidth < 1500 ? 1500.0 : constraints.maxWidth; var h = constraints.maxHeight; if (!h.isFinite || h <= 0) { h = MediaQuery.sizeOf(context).height * 0.45; } return Scrollbar( thumbVisibility: true, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox( width: w, height: h, child: ListView( padding: EdgeInsets.zero, children: [ _tableHeader(), ...pageItems.asMap().entries.map((e) { final i = e.key; return _dataRow(e.value, startIndex + i + 1); }), ], ), ), ), ); }, ); } /// Filters + table only (title row lives in [build] like [policylist]). Widget _buildContent(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildFilterBar(context), _buildSummaryChips(), const SizedBox(height: 5), ResponsiveLayout.isMobile(context) ? _buildDataTable(context) : Expanded(child: _buildDataTable(context)), ], ); } @override Widget build(BuildContext context) { final isMobile = ResponsiveLayout.isMobile(context); return MainLayout( title: 'Payout Report', // Single Column under MainLayout's Expanded — do not nest LayoutBuilder here; // nested LayoutBuilder + Expanded caused _debugRelayoutBoundaryAlreadyMarkedNeedsLayout on web. body: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( height: 30, child: Align( alignment: Alignment.centerLeft, child: Text( 'Payout Report', style: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.w500, ), ), ), ), const SizedBox(height: 5), Expanded( child: Padding( padding: const EdgeInsets.all(8.0), child: isMobile ? SingleChildScrollView( child: _buildContent(context), ) : _buildContent(context), ), ), PaginationControls( currentPage: currentPage, itemsPerPage: itemsPerPage, totalItems: _filtered.length, onPageChanged: (page) { setState(() => currentPage = page); }, onItemsPerPageChanged: (items) { setState(() { itemsPerPage = items; currentPage = 1; }); }, ), ], ), ); } }