enrollment-app/lib/presentation/nonEBClaimsList.dart
2026-07-28 10:40:36 +05:30

1784 lines
56 KiB
Dart

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<NonEBClaimsList> createState() => _NonEBClaimsListState();
}
class _NonEBClaimsListState extends State<NonEBClaimsList> {
List<String> tabHeader = [
'name',
'empCode',
'from',
'to',
'claimNum',
'policyNumber'
];
final tokenService = TokenStorageService();
List<Map<String, dynamic>> 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<String, TextEditingController> controllers = {};
List<dynamic> reversedDataPolicy = [];
List<Map<String, dynamic>> originalData = [];
String? selectedPolicyTypeName;
String? selectedClaimStatusName;
int? selectedPolicyType;
int? selectedClaimStatus;
List<Map<String, dynamic>> getClaimPolicies = [];
Map<String, dynamic> getClaimNonEBApi = {};
bool isLoading = false;
late ApiService apiService;
int _currentPage = 1;
int _rowsPerPage = 5;
List<dynamic> 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<String, dynamic> 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<void> _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<void> _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<void> 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<dynamic>.from(data)};
} else if (data is Map<String, dynamic>) {
getClaimNonEBApi = Map<String, dynamic>.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<void> 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<Map<String, dynamic>>.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<String, dynamic> item) {
return (item['status_display'] ?? item['status'] ?? '').toString();
}
int getStatusCount(String status) {
return filteredData
.where((item) =>
_claimStatusLabel(item).toLowerCase() == status.toLowerCase())
.length;
}
List<String> _uniqueDisplayNames(List<dynamic> statusMaster) {
final seen = <String>{};
final result = <String>[];
for (final status in statusMaster) {
final name = status['display_name']?.toString() ?? '';
if (name.isNotEmpty && seen.add(name.toLowerCase())) {
result.add(name);
}
}
return result;
}
Future<void> 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<void> 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<Map<String, dynamic>> data) {
List<List<String>> 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<void> 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<bool> _showLogoutDialog() async {
return await showDialog<bool>(
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<Map<String, dynamic>> get claimStatusList {
final list = _claimStatusMaster;
if (list is! List) return [];
return list
.map<Map<String, dynamic>>((e) => {
'id': int.parse(e['id'].toString()),
'name': e['display_name'].toString(),
})
.toList();
}
List<dynamic> 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<String, dynamic> 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<dynamic> statusMaster = _claimStatusMaster;
if (statusMaster.isEmpty) return const SizedBox();
final List<String> 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<ScrollMetricsNotification>(
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<int> 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<int> 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<int>(
value: _rowsPerPage,
// focusColor: Colors.transparent, // Fix: Removes the grey/blue highlight on change
items: [5, 10, 15, 20, 50].map((int value) {
return DropdownMenuItem<int>(
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<Map<String, dynamic>> 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<int>(
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<int>(
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<PointerDeviceKind> 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),
),
),
),
);
}
}