import 'dart:convert'; import 'package:csv/csv.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_search/dropdown_search.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:http/http.dart' as http; import 'package:collection/collection.dart'; import 'package:jwt_decode/jwt_decode.dart'; import 'package:universal_html/html.dart' as html; import '../service/secure_pop_scope.dart'; import 'claimshistory.dart'; import '../customAppBar/base_layout.dart'; import '../customAppBar/toastHelper.dart'; import '../service/api_service.dart'; import '../service/token_storage_service.dart'; import 'RaiseClaimForm.dart'; import 'package:nhancepolicy/logger.dart'; import 'nonEBClaimsCreate.dart'; import 'nonEBClaimsHistory.dart'; class NonEBClaimsList extends StatefulWidget { final String empCode; const NonEBClaimsList({ Key? key, required this.empCode, }); @override State createState() => _NonEBClaimsListState(); } class _NonEBClaimsListState extends State { List tabHeader = [ 'name', 'empCode', 'from', 'to', 'claimNum', 'policyNumber' ]; final tokenService = TokenStorageService(); List> filteredData = []; // Filtered data source dynamic empClientId; dynamic empClientBranchId; dynamic empHrId; String? _postPreToken = ''; TextEditingController searchController = TextEditingController(); TextEditingController searchClaimsStatusController = TextEditingController(); final ScrollController _statusScrollController = ScrollController(); bool _canScrollStatusLeft = false; bool _canScrollStatusRight = false; Map controllers = {}; List reversedDataPolicy = []; List> originalData = []; String? selectedPolicyTypeName; String? selectedClaimStatusName; int? selectedPolicyType; int? selectedClaimStatus; List> getClaimPolicies = []; Map getClaimNonEBApi = {}; bool isLoading = false; late ApiService apiService; int _currentPage = 1; int _rowsPerPage = 5; List get _paginatedData { final startIndex = (_currentPage - 1) * _rowsPerPage; final endIndex = (_currentPage * _rowsPerPage).clamp(0, filteredData.length); return filteredData.sublist(startIndex, endIndex); } int? appliedClaimStatus; Color getStatusColor(String status) { final normalized = status.toLowerCase().trim(); if (normalized == 'rejected' || normalized == 'claim rejected' || normalized.contains('rejected')) { return const Color(0xFFE26728); // Export button orange } if (normalized == 'settled' || normalized == 'claim settled' || normalized.contains('settled')) { return const Color(0xFF2ECC71); // Green } return const Color(0xFF00A6A6); // List header color } Map claim_Detials() { final data = { "page": 1, "per_page": 200, "client_id": empClientId ?? '', "date_type": "created_date", // "client_branch_id": empClientBranchId ?? '', // "emp_code": controllers['empCode']?.text ?? '', "start_date": controllers["from"]?.text ?? '', "end_date": controllers["to"]?.text ?? '', // "ticket_type_id": selectedPolicyType ?? '', "claim_status_id": selectedClaimStatus ?? '', "policy_no": controllers["policyNumber"]?.text ?? '', // "acm_name": controllers["name"]?.text ?? '', }; return data; } @override void initState() { super.initState(); apiService = ApiService(context); for (String field in tabHeader) { controllers[field] = TextEditingController(); } _statusScrollController.addListener(_updateStatusScrollArrows); WidgetsBinding.instance .addPostFrameCallback((_) => _updateStatusScrollArrows()); // if (_postPreToken != null && _postPreToken!.isNotEmpty) { // // claim_Detials(); // // logDebug("Empcoed1- ${widget.empCodeHrPolicy}"); // logDebug("EmpisHrcode- ${empHrId}"); // if (widget.empCodeHrPolicy != '') { // logDebug("Empcoedbool- ${widget.isHrcode}"); // controllers['empCode']!.text = widget.empCodeHrPolicy; // // if (widget.isHrcode == 0) { // // controllers['empCode']?.clear(); // // } else { // // controllers['empCode']!.text = widget.empCodeHrPolicy; // // } // // logDebug("Empcoed- ${widget.empCodeHrPolicy}"); // } // // // if (widget.empCodeHrPolicy != '' && widget.isHrcode) { // // logDebug("Empcoedbool- ${widget.isHrcode}"); // // controllers['empCode']!.text = widget.empCodeHrPolicy; // // logDebug("Empcoed- ${widget.empCodeHrPolicy}"); // // } // // getClaimList(); // } // getApiData(); _checkToken(); } Future _checkToken() async { final token = await tokenService.getCurrentToken(); if (token == null || token.trim().isEmpty) { if (!mounted) return; ToastHelper.showErrorToast(context, 'Session Out'); Navigator.pushNamedAndRemoveUntil( context, 'hrLogin', (route) => false, ); return; } await _loadIds(); } Future _loadIds() async { _postPreToken = tokenService.getCurrentToken(); empClientId = await tokenService.readValue('empClientId'); empClientBranchId = await tokenService.readValue('empClientBranchId'); logDebug('empClientId0000 $empClientId'); getClaimsPoliciesDetails(); // ✅ If empCode passed → auto filter if (widget.empCode.isNotEmpty) { setState(() { controllers['empCode']?.text = widget.empCode; }); await getClaimList(); } else { await getClaimList(); } } @override void dispose() { _statusScrollController.dispose(); for (var controller in controllers.values) { controller.dispose(); } controllers['empCode']?.clear(); super.dispose(); } Future getClaimsPoliciesDetails() async { logDebug('9'); setState(() { isLoading = true; }); try { logDebug('10'); final response = await apiService.getNonEBStatusToApi(_postPreToken!, empClientId); if (response['status'] == 'success' || response['status'] == true) { setState(() { isLoading = false; }); setState(() { final data = response['data']; if (data is List) { // Non-EB API currently returns status master directly as a list. getClaimNonEBApi = {'display_name': List.from(data)}; } else if (data is Map) { getClaimNonEBApi = Map.from(data); } else { getClaimNonEBApi = {}; } }); } else { setState(() { isLoading = false; }); // ToastHelper.showWarningToast( // context, 'Request failed with status: ${response.statusCode}'); logDebug('Request failed with status: ${response['code']}'); } } catch (e) { setState(() { // isLoading = false; }); logDebug('Exception occurred: $e'); } finally { setState(() { // _isLoading = false; }); } } Future getClaimList() async { logDebug('getClaimList called'); setState(() { isLoading = true; }); /// ✅ Resolve futures FIRST empClientId ??= await tokenService.readValue('empClientId'); empClientBranchId ??= await tokenService.readValue('empClientBranchId'); final requestData = claim_Detials(); logDebug("Request Body: $requestData"); try { final response = await apiService.getNonEBClaimsListDataToApi( _postPreToken!, requestData, ); if (response['status'] == 'success' || response['status'] == true) { setState(() { getClaimPolicies = List>.from(response['data']); logDebug('API Data - $getClaimPolicies'); originalData = getClaimPolicies; filteredData = List.from(originalData); logDebug('originalData - $getClaimPolicies'); }); } else { getClaimPolicies = []; originalData = getClaimPolicies; filteredData = List.from(originalData); logDebug('originalData - $getClaimPolicies'); logDebug('Request failed: ${response['code']}'); } } catch (e) { logDebug('Exception occurred: $e'); } finally { setState(() { isLoading = false; }); } } String _claimStatusLabel(Map item) { return (item['status_display'] ?? item['status'] ?? '').toString(); } int getStatusCount(String status) { return filteredData .where((item) => _claimStatusLabel(item).toLowerCase() == status.toLowerCase()) .length; } List _uniqueDisplayNames(List statusMaster) { final seen = {}; final result = []; for (final status in statusMaster) { final name = status['display_name']?.toString() ?? ''; if (name.isNotEmpty && seen.add(name.toLowerCase())) { result.add(name); } } return result; } Future applyFilter() async { logDebug("Filtersss"); /// ✅ Resolve futures FIRST empClientId ??= await tokenService.readValue('empClientId'); empClientBranchId ??= await tokenService.readValue('empClientBranchId'); final data = claim_Detials(); logDebug("data -- $data"); getClaimList(); setState(() { appliedClaimStatus = selectedClaimStatus; // ✅ apply only after API call }); } Future reset() async { setState(() { controllers.forEach((key, controller) { controller.clear(); }); selectedPolicyType = null; selectedPolicyTypeName = null; selectedClaimStatus = null; selectedClaimStatusName = null; _currentPage = 1; appliedClaimStatus = null; getClaimList(); }); /// ✅ Resolve futures FIRST empClientId ??= await tokenService.readValue('empClientId'); empClientBranchId ??= await tokenService.readValue('empClientBranchId'); final data = claim_Detials(); logDebug("restdata -- $data"); } void exportToCsv(List> data) { List> rows = []; // Header rows.add([ 'Name', 'Insured Name', 'Policy Name', 'Policy Number', 'Claim Number', 'Status', ]); // Data rows for (var item in data) { rows.add([ item['acm_name'] ?? '', item['insured_contact_name'] ?? '', item['policy_type_name'] ?? '', item['policy_no'] ?? '', item['claim_number'] ?? '', item['status_display'] ?? '', ]); } // Convert to CSV string String csvData = const ListToCsvConverter().convert(rows); // For Web: Create download final bytes = utf8.encode(csvData); final blob = html.Blob([bytes]); final url = html.Url.createObjectUrlFromBlob(blob); final anchor = html.AnchorElement(href: url) ..setAttribute("download", "Claims.csv") ..click(); html.Url.revokeObjectUrl(url); handleExportAction(); } Future handleExportAction() async { logDebug('handleExportAction'); _postPreToken = await tokenService.getCurrentToken(); final postId = await tokenService.readValue('empHrId'); final preId = await tokenService.readValue('enrollmentEmpPrimaryId'); var activity = "export_cddata"; logDebug('postId - $postId'); logDebug('preId - $preId'); logDebug('activity - $activity'); try { logDebug('10'); final response = await apiService.getPostLogHrActivity( postId!, preId!, _postPreToken!, activity); if (response['status'] == 'success') { logDebug('Request success'); } else { // ToastHelper.showWarningToast( // context, 'Request failed with status: ${response.statusCode}'); logDebug('Request failed with status: ${response['code']}'); } } catch (e) { logDebug('Exception occurred: $e'); } } void search(String query) { logDebug(query); // Check if the query is empty if (query.isEmpty) { // If search query is empty, show all data setState(() { filteredData = List.from(originalData); }); } else { // Filter the original data based on the search query setState(() { filteredData = originalData.where((row) { // Implement your filter logic here // For example, check if any field in the row contains the query // Adjust this logic based on your data structure return row['acm_name'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['acm_name'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['insured_contact_name'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['policy_type_name'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['policy_no'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['claim_number'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['status_display'] .toString() .toLowerCase() .contains(query.toLowerCase()); }).toList(); }); } logDebug(filteredData.length); } Future _showLogoutDialog() async { return await showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: Text("Confirm Logout"), content: Text("Do you want to logout?"), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: Text("Cancel"), ), TextButton( onPressed: () => Navigator.of(context).pop(true), child: Text("Logout"), ), ], ), ) ?? false; } // ✅ PUT IT HERE (inside State, outside build) List> get claimStatusList { final list = _claimStatusMaster; if (list is! List) return []; return list .map>((e) => { 'id': int.parse(e['id'].toString()), 'name': e['display_name'].toString(), }) .toList(); } List get _claimStatusMaster { final directList = getClaimNonEBApi['display_name']; if (directList is List) return directList; final fallbackData = getClaimNonEBApi['data']; if (fallbackData is List) return fallbackData; return []; } @override Widget build(BuildContext context) { return BaseLayout( child: SecurePopScope( child: _buildContent(context), ), ); } Widget _buildContent(BuildContext context) { return Container( decoration: BoxDecoration( // color: Colors.red.shade50, // color: Colors.white, borderRadius: BorderRadius.circular(10), // 👈 set your desired radius ), padding: const EdgeInsets.all(16.0), child: Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ /// 🔙 Back + Title (LEFT) Row( children: [ if (widget.empCode.isNotEmpty) IconButton( onPressed: () => { Navigator.of(context).pop(), }, icon: const Icon( Icons.arrow_back_ios, size: 18, color: Colors.black, ), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), const SizedBox(width: 6), Text( 'Claims', style: GoogleFonts.poppins( fontSize: 22, fontWeight: FontWeight.w500, color: Colors.black, ), ), ], ), /// Push right content to end const Spacer(), /// 🔍 Search Box Container( width: 380, height: 37, decoration: BoxDecoration( color: const Color(0xFFF0F0F0), borderRadius: BorderRadius.circular(8), ), child: TextField( controller: searchController, onChanged: search, style: GoogleFonts.poppins(fontSize: 14), decoration: const InputDecoration( hintText: 'Search', prefixIcon: Icon(Icons.search, size: 18), border: InputBorder.none, contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), ), ), const SizedBox(width: 12), /// ⬇️ Export Button SizedBox( width: 116, height: 37, child: ElevatedButton( onPressed: () { exportToCsv(filteredData); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFE26728), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), child: Text( 'Export', style: GoogleFonts.poppins( fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white, letterSpacing: 1, ), ), ), ), const SizedBox(width: 12), /// ⬇️ Add Button SizedBox( width: 100, height: 37, child: ElevatedButton( onPressed: () { showDialog( context: context, barrierDismissible: true, builder: (_) => NonEBClaimsCreate( parentContext: context, // 👈 pass parent context onSuccess: _loadIds, ), ); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF00999E), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), child: Text( 'Add', style: GoogleFonts.poppins( fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white, letterSpacing: 1, ), ), ), ), ], ), SizedBox(height: 20), _buildInputFields(context), const SizedBox(height: 10), _buildStatusSummary(), SizedBox( height: 10, ), isLoading ? Expanded( // color: Color(0x98FFFCE5), // Semi-transparent background child: Center( child: // Your GIF loader widget Image.asset( height: 60, width: 60, 'assets/nhance-loader.gif'), // Adjust path to your GIF loader ), ) : Expanded( child: _buildClaimsDataTable(context), // ✅ THIS IS REQUIRED ), // Expanded( // child: Container( // child: Column( // children: [ // // Text("DAer"), // // _buildClaimsDataTable(context), // SingleChildScrollView( // scrollDirection: Axis.vertical, // child: _buildClaimsDataTable(context), // ), // ], // ), // ), // ), ], ), ); } Widget _buildInputFields(BuildContext context) { return SizedBox( height: 70, // 🔥 controls total height like image child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ _buildFrom(context), const SizedBox(width: 12), _buildTo(context), const SizedBox(width: 12), _buildPolicyNumber(context), const SizedBox(width: 12), _buildClaimStatus(context), const SizedBox(width: 12), _buildActions(context), ], ), ); } Widget _buildClaimStatus(BuildContext context) { return SizedBox( width: MediaQuery.of(context).size.width * 0.15, child: buildDropdownFieldSearch( 'Claim Status', (int? value) { setState(() { selectedClaimStatus = value; }); logDebug('Selected Claim Status ID: $selectedClaimStatus'); }, claimStatusList, 'name', selectedClaimStatus, ), ); } Widget _buildClaimsDataTable(BuildContext context) { if (filteredData.isEmpty) { return const Center(child: Text('No available data')); } return CustomScrollView( slivers: [ /// 🔒 FIXED HEADER SliverPersistentHeader( pinned: true, delegate: _ClaimsHeaderDelegate(), ), /// 📄 TABLE ROWS SliverList( delegate: SliverChildBuilderDelegate( (context, index) { final item = _paginatedData[index]; return _buildDataRow(item); }, childCount: _paginatedData.length, ), ), /// 📌 PAGINATION SliverToBoxAdapter( child: _buildPagination(context), ), ], ); } Widget _buildDataRow(Map item) { final statusLabel = _claimStatusLabel(item); return Container( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), margin: const EdgeInsets.only(top: 10), decoration: BoxDecoration( color: Color(0xFFE0F7F9), border: const Border( bottom: BorderSide(color: Color(0xFFD7E9EB), width: 1), ), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Expanded( flex: 4, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item['acm_name'] ?? '-', style: _dataBold), // Text(item['emp_code'] ?? '-', style: _dataSub), ], ), ), Expanded( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item['insured_contact_name'] ?? '-', style: _dataBold), ], ), ), Expanded( flex: 4, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item['policy_type_name'] ?? '-', style: _dataBold), Text(item['policy_no'] ?? '-', style: _dataSub), ], ), ), Expanded( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item['claim_number'] ?? '-', style: _dataBold), // Text(item['cl_type'] ?? '-', style: _dataColorSub), ], ), ), Expanded( flex: 4, child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Container( padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: getStatusColor(statusLabel), borderRadius: BorderRadius.circular(10), ), child: Text( statusLabel.isEmpty ? '-' : statusLabel, style: _dataBoldStatus)), ], ), ), // Expanded( // flex: 3, // child: Text(item['claim_amount'] ?? '-', style: _dataBold), // ), // Expanded( // flex: 3, // child: Text(item['ticket_created_date'] ?? '-', style: _dataBold), // ), Expanded( flex: 2, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // 1. Tooltip for the hover message Tooltip( message: 'View Claim History', child: // 2. MouseRegion for the hand pointer MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () { showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) { return Dialog( backgroundColor: Colors.transparent, insetPadding: EdgeInsets.all(16), child: NonEBClaimsHistory( ticket_id: item['id'] ?? '', empName: item['acm_name'] ?? '', // example // empCode: item['emp_code'] ?? '', // example policyType: item['policy_type_name'] ?? '', clientPolicyNo: item['policy_no'] ?? '', // claimAmount: // item['claim_amount']?.toString() ?? '', claimNo: item['claim_number'] ?? '', postToken: _postPreToken ?? '', ), ); }, ); }, child: Container( height: 35, width: 35, decoration: BoxDecoration( color: Color(0xFF86D1D4), borderRadius: BorderRadius.circular(8), ), child: Padding( padding: EdgeInsets.all(10), // You can adjust this value child: Image.asset( 'assets/claimHistory.png', fit: BoxFit.contain, width: 5, ), ), ), ), ), ), ], ), ), ], ), ); } void _updateStatusScrollArrows() { if (!_statusScrollController.hasClients) return; final position = _statusScrollController.position; final canLeft = position.pixels > position.minScrollExtent + 1; final canRight = position.pixels < position.maxScrollExtent - 1; if (canLeft != _canScrollStatusLeft || canRight != _canScrollStatusRight) { setState(() { _canScrollStatusLeft = canLeft; _canScrollStatusRight = canRight; }); } } void _scrollStatusBy(double delta) { if (!_statusScrollController.hasClients) return; final target = (_statusScrollController.offset + delta) .clamp(0.0, _statusScrollController.position.maxScrollExtent); _statusScrollController.animateTo( target, duration: const Duration(milliseconds: 280), curve: Curves.easeOutCubic, ); } Widget _buildStatusScrollButton({ required IconData icon, required bool enabled, required VoidCallback onTap, }) { return SizedBox( width: 32, height: 40, child: Material( color: enabled ? const Color(0xFF00999E) : const Color(0xFFE8E8E8), borderRadius: BorderRadius.circular(8), child: InkWell( onTap: enabled ? onTap : null, borderRadius: BorderRadius.circular(8), child: Icon( icon, size: 22, color: enabled ? Colors.white : Colors.black38, ), ), ), ); } Widget _buildStatusSummary() { final List statusMaster = _claimStatusMaster; if (statusMaster.isEmpty) return const SizedBox(); final List displayStatuses = appliedClaimStatus != null ? statusMaster .where((status) => int.parse(status['id'].toString()) == appliedClaimStatus) .map((status) => status['display_name']?.toString() ?? '') .where((name) => name.isNotEmpty) .toList() : _uniqueDisplayNames(statusMaster); return Row( children: [ _buildStatusScrollButton( icon: Icons.chevron_left, enabled: _canScrollStatusLeft, onTap: () => _scrollStatusBy(-240), ), const SizedBox(width: 8), Expanded( child: ScrollConfiguration( behavior: _StatusDragScrollBehavior(), child: MouseRegion( cursor: SystemMouseCursors.grab, child: SizedBox( height: 40, child: NotificationListener( onNotification: (_) { _updateStatusScrollArrows(); return false; }, child: ListView.separated( controller: _statusScrollController, scrollDirection: Axis.horizontal, physics: const ClampingScrollPhysics(), itemCount: displayStatuses.length, separatorBuilder: (_, __) => const SizedBox(width: 10), itemBuilder: (context, index) { final statusName = displayStatuses[index]; final count = getStatusCount(statusName); final color = getStatusColor(statusName); return Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(6), ), child: Text( '$statusName - $count', softWrap: false, style: GoogleFonts.poppins( fontSize: 12, fontWeight: FontWeight.w500, color: Colors.white, ), ), ); }, ), ), ), ), ), ), const SizedBox(width: 8), _buildStatusScrollButton( icon: Icons.chevron_right, enabled: _canScrollStatusRight, onTap: () => _scrollStatusBy(240), ), ], ); } static final _dataBold = GoogleFonts.poppins( fontSize: 12, fontWeight: FontWeight.w400, color: Color(0xFF000000), ); static final _dataBoldStatus = GoogleFonts.poppins( fontSize: 12, fontWeight: FontWeight.w400, color: Colors.white, ); static final _dataSub = GoogleFonts.poppins( fontSize: 10, fontWeight: FontWeight.w300, color: Color(0xFF585757), ); static final _dataColorSub = GoogleFonts.poppins( fontSize: 10, fontWeight: FontWeight.w400, color: Color(0xFFFF731C), ); static const _headerStyle = TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ); Widget _buildPagination(BuildContext context) { // 1. Calculate the range of entries being shown final totalItems = filteredData.length; final int startEntry = totalItems == 0 ? 0 : ((_currentPage - 1) * _rowsPerPage) + 1; int endEntry = _currentPage * _rowsPerPage; if (endEntry > totalItems) endEntry = totalItems; final totalPages = (filteredData.length / _rowsPerPage).ceil(); const visiblePageCount = 5; List getVisiblePages() { if (totalPages <= visiblePageCount) { return List.generate(totalPages, (i) => i + 1); } if (_currentPage <= 3) { return [1, 2, 3, 4, 5]; } if (_currentPage >= totalPages - 2) { return [ totalPages - 4, totalPages - 3, totalPages - 2, totalPages - 1, totalPages ]; } return [ _currentPage - 2, _currentPage - 1, _currentPage, _currentPage + 1, _currentPage + 2, ]; } List visiblePages = getVisiblePages(); return Padding( // Match this horizontal padding (16) to your Table Header padding for perfect alignment padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, // Pushes text to left, buttons to right children: [ // --- LEFT SIDE: Showing Text --- Text( "Showing $startEntry to $endEntry of $totalItems entries", style: GoogleFonts.poppins( fontSize: 13, color: const Color(0xFF585757), fontWeight: FontWeight.w400, ), ), // --- RIGHT SIDE: Controls --- Row( children: [ // Dropdown for rows per page DropdownButton( value: _rowsPerPage, // focusColor: Colors.transparent, // Fix: Removes the grey/blue highlight on change items: [5, 10, 15, 20, 50].map((int value) { return DropdownMenuItem( value: value, child: Text(' $value ', style: GoogleFonts.poppins(fontSize: 15)), ); }).toList(), onChanged: (newValue) { setState(() { _rowsPerPage = newValue!; _currentPage = 1; }); }, ), // Previous button IconButton( onPressed: _currentPage > 1 ? () => setState(() => _currentPage--) : null, icon: const Icon(Icons.chevron_left), ), // First page + left ellipsis if (!visiblePages.contains(1)) Row(children: [ _buildPageButton(1), const Padding( padding: EdgeInsets.symmetric(horizontal: 4), child: Text("..."), ), ]), // Visible page buttons for (int page in visiblePages) _buildPageButton(page), // Right ellipsis + last page if (!visiblePages.contains(totalPages) && totalPages > 0) Row(children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 4), child: Text("..."), ), _buildPageButton(totalPages), ]), // Next button IconButton( onPressed: _currentPage < totalPages ? () => setState(() => _currentPage++) : null, icon: const Icon(Icons.chevron_right), ), ], ), ], ), ); } Widget _buildPageButton(int page) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: _currentPage == page ? const Color(0xFF00A6A6) : Colors.grey[300], foregroundColor: _currentPage == page ? Colors.white : Colors.black, minimumSize: const Size(36, 36), padding: EdgeInsets.zero, ), onPressed: () { setState(() { _currentPage = page; }); }, child: Text(page.toString()), ), ); } Widget _buildName(BuildContext context) { return Container( width: MediaQuery.of(context).size.width * 0.22, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "Name", style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500), ), const SizedBox( height: 3, ), SizedBox( height: 40, child: TextField( controller: controllers['name'], style: const TextStyle( fontSize: 12, ), decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.never, hintText: 'Enter your name', filled: true, fillColor: Color(0xFFD5F5F6), border: OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent), borderRadius: BorderRadius.circular(8.0), ), enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent), borderRadius: BorderRadius.circular(8.0), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.teal, width: 2.0), borderRadius: BorderRadius.circular(8.0), ), // contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), contentPadding: EdgeInsets.fromLTRB(12, 20, 12, 10), ), ), ), ], ), ); } Widget _buildEmpCode(BuildContext context) { return Container( width: MediaQuery.of(context).size.width * 0.22, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Emp Code", style: TextStyle(fontSize: 12), ), SizedBox( height: 3, ), SizedBox( height: 40, child: TextField( controller: controllers['empCode'], style: TextStyle( fontSize: 12, ), decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.never, hintText: 'Enter your Emp Code', filled: true, fillColor: Color(0xFFD5F5F6), border: OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent), borderRadius: BorderRadius.circular(8.0), ), enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent), borderRadius: BorderRadius.circular(8.0), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.teal, width: 2.0), borderRadius: BorderRadius.circular(8.0), ), // contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), contentPadding: EdgeInsets.fromLTRB(12, 20, 12, 10), ), ), ), ], ), ); } Widget _buildFrom(BuildContext context) { return Container( width: MediaQuery.of(context).size.width * 0.15, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "From", style: TextStyle(fontSize: 12), ), const SizedBox( height: 3, ), SizedBox( height: 40, child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), // soft shadow blurRadius: 8, offset: const Offset(0, 2), // downward shadow ), ], ), child: TextField( controller: controllers['from'], readOnly: true, onTap: () async { FocusScope.of(context) .requestFocus(FocusNode()); // hide keyboard DateTime? pickedDate = await showDatePicker( context: context, initialDate: DateTime.now(), firstDate: DateTime(2000), lastDate: DateTime.now(), ); if (pickedDate != null) { String formattedDate = "${pickedDate.day.toString().padLeft(2, '0')}-${pickedDate.month.toString().padLeft(2, '0')}-${pickedDate.year}"; logDebug("FomatedFromDAta - $formattedDate"); setState(() { controllers['from']?.text = formattedDate; controllers['to'] ?.clear(); // 🔥 prevent invalid To date }); } }, style: const TextStyle( fontSize: 12, ), decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.never, hintText: 'Select', filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent), borderRadius: BorderRadius.circular(8.0), ), enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent), borderRadius: BorderRadius.circular(8.0), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.teal, width: 2.0), borderRadius: BorderRadius.circular(8.0), ), // contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), contentPadding: EdgeInsets.fromLTRB(12, 20, 12, 10), suffixIcon: const Icon(Icons.calendar_today, size: 16), ), ), )), ], ), ); } Widget _buildTo(BuildContext context) { return Container( width: MediaQuery.of(context).size.width * 0.15, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "To", style: TextStyle(fontSize: 12), ), const SizedBox( height: 3, ), SizedBox( height: 40, child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), // soft shadow blurRadius: 8, offset: const Offset(0, 2), // downward shadow ), ], ), child: TextField( controller: controllers['to'], readOnly: true, onTap: () async { FocusScope.of(context).requestFocus(FocusNode()); final fromDate = _parseDate(controllers['from']?.text); DateTime? pickedDate = await showDatePicker( context: context, initialDate: fromDate ?? DateTime.now(), firstDate: fromDate ?? DateTime(2000), // ✅ cannot be before From date lastDate: DateTime.now(), // ✅ no future dates ); if (pickedDate != null) { String formattedDate = "${pickedDate.day.toString().padLeft(2, '0')}-" "${pickedDate.month.toString().padLeft(2, '0')}-" "${pickedDate.year}"; setState(() { controllers['to']?.text = formattedDate; }); } }, style: const TextStyle( fontSize: 12, ), decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.never, hintText: 'Select', filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent), borderRadius: BorderRadius.circular(8.0), ), enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent), borderRadius: BorderRadius.circular(8.0), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.teal, width: 2.0), borderRadius: BorderRadius.circular(8.0), ), // contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), contentPadding: EdgeInsets.fromLTRB(12, 20, 12, 10), suffixIcon: const Icon(Icons.calendar_today, size: 16), ), ), )), ], ), ); } Widget buildDropdownFieldSearch( String label, void Function(int?) onChanged, List> itemsList, String displayField, int? selectedValue, ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), ), const SizedBox(height: 4), Container( height: 40, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: DropdownButtonHideUnderline( child: DropdownButton2( isExpanded: true, value: selectedValue, hint: const Text( 'Select', style: TextStyle(fontSize: 12), ), iconStyleData: const IconStyleData( icon: Icon(Icons.keyboard_arrow_down), ), dropdownStyleData: DropdownStyleData( maxHeight: 260, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), ), ), // 🔍 SEARCH SUPPORT dropdownSearchData: DropdownSearchData( searchController: searchClaimsStatusController, searchInnerWidgetHeight: 50, searchInnerWidget: Padding( padding: const EdgeInsets.all(8), child: TextField( controller: searchClaimsStatusController, style: const TextStyle(fontSize: 12), decoration: InputDecoration( hintText: 'Search...', hintStyle: const TextStyle(fontSize: 12), isDense: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), ), ), searchMatchFn: (item, searchValue) { final matchedItem = itemsList.firstWhere( (e) => e['id'] == item.value, orElse: () => {}, ); final text = matchedItem['name']?.toString().toLowerCase() ?? ''; return text.contains(searchValue.toLowerCase()); }, ), onMenuStateChange: (isOpen) { if (!isOpen) { searchClaimsStatusController.clear(); } }, items: itemsList.map((item) { return DropdownMenuItem( value: item['id'], child: Text( item[displayField] ?? '', style: const TextStyle(fontSize: 12), overflow: TextOverflow.ellipsis, ), ); }).toList(), onChanged: onChanged, ), ), ), ], ); } DateTime? _parseDate(String? value) { if (value == null || value.isEmpty) return null; final parts = value.split('-'); return DateTime( int.parse(parts[2]), int.parse(parts[1]), int.parse(parts[0]), ); } Widget _buildPolicyNumber(BuildContext context) { return Container( width: MediaQuery.of(context).size.width * 0.15, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "Policy Number", style: TextStyle(fontSize: 12), ), const SizedBox( height: 3, ), SizedBox( height: 40, child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), // soft shadow blurRadius: 8, offset: const Offset(0, 2), // downward shadow ), ], ), child: TextField( controller: controllers['policyNumber'], style: const TextStyle( fontSize: 12, ), decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.never, hintText: 'Policy Number', filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent), borderRadius: BorderRadius.circular(8.0), ), enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent), borderRadius: BorderRadius.circular(8.0), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.teal, width: 2.0), borderRadius: BorderRadius.circular(8.0), ), // contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), contentPadding: EdgeInsets.fromLTRB(12, 20, 12, 10), ), ), )), ], ), ); } Widget _buildActions(BuildContext context) { return SizedBox( width: 100, // enough for 2 icons child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ /// 🔹 Empty label space (matches "From / To / Policy Number") const SizedBox( height: 20, // same height as label text + spacing ), /// 🔹 Icon row aligned with input fields SizedBox( height: 40, // EXACT same height as input fields child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ _IconActionButton( icon: Icons.filter_alt_outlined, onTap: applyFilter, tooltip: "Filter"), const SizedBox(width: 12), _IconActionButton( icon: Icons.refresh_outlined, onTap: reset, tooltip: "Reset"), ], ), ), ], ), ); } } class _ClaimsHeaderDelegate extends SliverPersistentHeaderDelegate { @override double get minExtent => 52; @override double get maxExtent => 52; @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { return Container( // color: const Color(0xFF00A6A6), decoration: BoxDecoration( color: Color(0xFF00A6A6), borderRadius: BorderRadius.circular(6), ), padding: const EdgeInsets.symmetric(horizontal: 16), alignment: Alignment.centerLeft, child: const Row( children: [ Expanded(flex: 4, child: Text('Name', style: _headerStyle)), Expanded(flex: 3, child: Text('Insured Name', style: _headerStyle)), Expanded(flex: 4, child: Text('Policy Name', style: _headerStyle)), Expanded(flex: 3, child: Text('Claim Number', style: _headerStyle)), Expanded(flex: 4, child: Text('Status', style: _headerStyle)), Expanded( flex: 2, child: Text('Action', textAlign: TextAlign.center, style: _headerStyle), ), ], ), ); } static const _headerStyle = TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ); @override bool shouldRebuild(_) => false; } class _StatusDragScrollBehavior extends MaterialScrollBehavior { @override Set get dragDevices => { PointerDeviceKind.touch, PointerDeviceKind.mouse, PointerDeviceKind.stylus, PointerDeviceKind.trackpad, }; } // ================= ICON BUTTON ================= class _IconActionButton extends StatelessWidget { final IconData icon; final VoidCallback onTap; final String tooltip; const _IconActionButton({ required this.icon, required this.onTap, required this.tooltip, }); @override Widget build(BuildContext context) { return Tooltip( message: tooltip, preferBelow: false, // Optional: Shows tooltip above the button child: SizedBox( width: 40, height: 40, child: Material( color: const Color(0xFF00999E), borderRadius: BorderRadius.circular(10), child: InkWell( borderRadius: BorderRadius.circular(10), onTap: onTap, child: Icon(icon, color: Colors.white, size: 20), ), ), ), ); } }