1784 lines
56 KiB
Dart
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),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|