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/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'; class ClaimsPolicies extends StatefulWidget { final String empCode; const ClaimsPolicies({ Key? key, required this.empCode, }); @override State createState() => _ClaimsPolicieState(); } class _ClaimsPolicieState 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(); Map controllers = {}; List reversedDataPolicy = []; List> originalData = []; String? selectedPolicyTypeName; String? selectedClaimStatusName; int? selectedPolicyType; int? selectedClaimStatus; List> getClaimPolicies = []; Map getClaimPoliciesApi = {}; 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.contains('rejected')) { return const Color(0xFFE26728); // Export button orange } if (normalized == 'settled' || normalized.contains('settled')) { return const Color(0xFF2ECC71); // Green } return const Color(0xFF00A6A6); // List header color } Map claim_Detials() { final data = { "client_id": empClientId ?? '', "client_branch_id": empClientBranchId ?? '', "emp_code": controllers['empCode']?.text ?? '', "from_date": controllers["from"]?.text ?? '', "to_date": controllers["to"]?.text ?? '', // "ticket_type_id": selectedPolicyType ?? '', "claim_status_id": selectedClaimStatus ?? '', "policy_no": controllers["policyNumber"]?.text ?? '', // "emp_name": controllers["name"]?.text ?? '', }; return data; } @override void initState() { super.initState(); apiService = ApiService(context); for (String field in tabHeader) { controllers[field] = TextEditingController(); } // 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() { 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.getClaimPoliciesToApi(_postPreToken!, empClientId); if (response['status'] == 'success') { setState(() { isLoading = false; }); setState(() { getClaimPoliciesApi = Map.from(response['data']); }); logDebug('getClaimPoliciesApi $getClaimPoliciesApi'); } 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.getClaimPoliciesListDataToApi( _postPreToken!, requestData, ); if (response['status'] == 'success') { 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; }); } } int getStatusCount(String status) { return filteredData .where((item) => (item['status'] ?? '').toString().toLowerCase() == status.toLowerCase()) .length; } 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([ 'Emp Code', 'Emp Name', 'Insured Name', 'Policy Type', 'Client Policy No', 'Claim Number', 'Claim Type', 'Status', 'Claim Amount', 'Record Date' ]); // Data rows for (var item in data) { rows.add([ item['emp_code'] ?? '', item['emp_name'] ?? '', item['insured_name'] ?? '', item['policy_type'] ?? '', item['client_policy_no'] ?? '', item['claim_no'] ?? '', item['cl_type'] ?? '', item['status'] ?? '', item['claim_amount'] ?? '', item['ticket_created_date'] ?? '', ]); } // 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['emp_name'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['emp_code'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['insured_name'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['policy_type'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['client_policy_no'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['claim_no'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['cl_type'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['status'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['claim_amount'] .toString() .toLowerCase() .contains(query.toLowerCase()) || row['ticket_created_date'] .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 = getClaimPoliciesApi['claim_status'] ?? []; if (list is! List) return []; return list .map>((e) => { 'id': int.parse(e['id'].toString()), 'name': e['claim_status'].toString(), }) .toList(); } @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: (_) => RaiseClaimDialog( 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) { 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['emp_name'] ?? '-', style: _dataBold), Text(item['emp_code'] ?? '-', style: _dataSub), ], ), ), Expanded( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item['insured_name'] ?? '-', style: _dataBold), ], ), ), Expanded( flex: 4, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item['policy_type'] ?? '-', style: _dataBold), Text(item['client_policy_no'] ?? '-', style: _dataSub), ], ), ), Expanded( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item['claim_no'] ?? '-', 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(item['status'] ?? ''), borderRadius: BorderRadius.circular(10), ), child: Text(item['status'] ?? '-', 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: () { // logDebug('id ${item['id']}'); // logDebug('emp_name ${item['emp_name']}'); // logDebug('emp_code ${item['emp_code']}'); // logDebug('policy_type ${item['policy_type']}'); // logDebug('client_policy_no ${item['client_policy_no']}'); // logDebug('claim_amount ${item['claim_amount']}'); // logDebug('claim_no ${item['claim_no']}'); // logDebug(widget.postToken); // return; showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) { return Dialog( backgroundColor: Colors.transparent, insetPadding: EdgeInsets.all(16), child: ClaimHistoryPopup( ticket_id: item['id'] ?? '', empName: item['emp_name'] ?? '', // example empCode: item['emp_code'] ?? '', // example policyType: item['policy_type'] ?? '', clientPolicyNo: item['client_policy_no'] ?? '', claimAmount: item['claim_amount']?.toString() ?? '', claimNo: item['claim_no'] ?? '', 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, ), ), ), ), ), ), ], ), ), ], ), ); } Widget _buildStatusSummary() { final List statusMaster = getClaimPoliciesApi['claim_status'] ?? []; if (statusMaster.isEmpty) return const SizedBox(); final List displayStatuses = appliedClaimStatus != null ? statusMaster .where((status) => int.parse(status['id'].toString()) == appliedClaimStatus) .toList() : statusMaster; return SizedBox( height: 35, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: displayStatuses.length, separatorBuilder: (_, __) => const SizedBox(width: 10), itemBuilder: (context, index) { final statusName = displayStatuses[index]['claim_status']?.toString() ?? ''; 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', style: GoogleFonts.poppins( fontSize: 12, fontWeight: FontWeight.w500, color: Colors.white, ), ), ); }, ), ); } 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 _buildPolicyType(BuildContext context) { List ticketTypeList = getClaimPoliciesApi['ticket_type'] ?? []; // Get type_name from ticket_type id String? getTypeNameById(int? id) { if (id == null) return null; final match = ticketTypeList.firstWhere( (e) => e['ticket_type'] == id, orElse: () => null, ); return match != null ? match['type_name'] : null; } // Get id from type_name int? getIdByTypeName(String? typeName) { logDebug("getIdByTypeName"); logDebug("getIdByTypeName - $typeName"); final match = ticketTypeList.firstWhere( (e) => e['type_name'] == typeName, orElse: () => null, ); logDebug("match - $match"); if (match != null && match is Map) { final map = match as Map; final id = map['ticket_type']; logDebug("Returning ticket_type: $id"); return id is int ? id : int.tryParse(id.toString()); } logDebug("no match or invalid map"); return null; } return Container( width: MediaQuery.of(context).size.width * 0.22, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "Policy Type", style: TextStyle(fontSize: 12), ), const SizedBox(height: 3), SizedBox( height: 40, child: DropdownSearch( selectedItem: selectedPolicyTypeName, dropdownBuilder: (context, selectedItem) { return Text( selectedItem ?? "", style: TextStyle(fontSize: 12), ); }, items: (String? filter, _) { return ticketTypeList .map((item) => item['type_name'].toString()) .toList(); }, onChanged: (value) { setState(() { selectedPolicyTypeName = value; selectedPolicyType = getIdByTypeName(value); }); logDebug( "Selected selectedPolicyTypeName: $selectedPolicyTypeName"); logDebug("Selected selectedPolicyType: $selectedPolicyType"); }, decoratorProps: DropDownDecoratorProps( decoration: InputDecoration( hintText: "Select Policy Type", hintStyle: TextStyle(fontSize: 12), filled: true, fillColor: Color(0xFFD5F5F6), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.teal, width: 2), ), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), ), ), popupProps: PopupProps.menu( menuProps: const MenuProps(color: Colors.red), fit: FlexFit.loose, constraints: const BoxConstraints(maxHeight: 250), showSearchBox: true, searchFieldProps: const TextFieldProps( decoration: InputDecoration( hintText: "Search Policy Type", hintStyle: TextStyle(fontSize: 12), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 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: 3, child: Text('Claim Amount', style: _headerStyle)), Expanded(flex: 3, child: Text('Record Date', 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; } // ================= 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), ), ), ), ); } }