From 3b323b4ce206ef9b622c94d40cf89b90f1473b42 Mon Sep 17 00:00:00 2001 From: SurendarSuri30 Date: Thu, 16 Apr 2026 17:57:22 +0530 Subject: [PATCH] non EB --- lib/customAppBar/side_bar.dart | 219 ++- lib/main.dart | 2 + lib/presentation/claims.dart | 4 +- lib/presentation/claimshistory.dart | 6 +- lib/presentation/hrPolicyDetails.dart | 17 +- lib/presentation/nonEBClaimsCreate.dart | 777 ++++++++++ lib/presentation/nonEBClaimsHistory.dart | 1642 +++++++++++++++++++++ lib/presentation/nonEBClaimsList.dart | 1657 ++++++++++++++++++++++ lib/service/api_service.dart | 66 + lib/service/nonEBClaimsService.dart | 107 ++ 10 files changed, 4460 insertions(+), 37 deletions(-) create mode 100644 lib/presentation/nonEBClaimsCreate.dart create mode 100644 lib/presentation/nonEBClaimsHistory.dart create mode 100644 lib/presentation/nonEBClaimsList.dart create mode 100644 lib/service/nonEBClaimsService.dart diff --git a/lib/customAppBar/side_bar.dart b/lib/customAppBar/side_bar.dart index 9c24620..fee9455 100644 --- a/lib/customAppBar/side_bar.dart +++ b/lib/customAppBar/side_bar.dart @@ -17,6 +17,10 @@ class NhanceSideBar extends StatefulWidget { class _NhanceSideBarState extends State { String? activeRoute; late ApiService apiService; + OverlayEntry? _claimsOverlayEntry; + bool _isHoveringClaimsItem = false; + bool _isHoveringClaimsMenu = false; + DateTime? _claimsMenuSuppressUntil; // bool isLoading = true; // Add a loading state // bool hideInactiveStatus = true; final tokenService = TokenStorageService(); @@ -109,17 +113,104 @@ class _NhanceSideBarState extends State { setState(() { activeRoute = newRoute; }); + _closeClaimsMenu(); // Re-verify the menu items if the route changes _buildSideMenu(); } } void _navigate(String routeName) { + _closeClaimsMenu(); if (activeRoute == routeName) return; // Navigation should happen first, didChangeDependencies will handle the state Navigator.pushReplacementNamed(context, routeName); } + void _showClaimsSubMenu(GlobalKey key) { + if (_claimsMenuSuppressUntil != null && + DateTime.now().isBefore(_claimsMenuSuppressUntil!)) { + return; + } + + final targetContext = key.currentContext; + if (targetContext == null) return; + + final box = targetContext.findRenderObject() as RenderBox?; + if (box == null) return; + + final offset = box.localToGlobal(Offset.zero); + _claimsOverlayEntry?.remove(); + _claimsOverlayEntry = OverlayEntry( + builder: (context) => Positioned( + left: offset.dx + box.size.width + 6, + top: offset.dy, + child: MouseRegion( + onEnter: (_) { + _isHoveringClaimsMenu = true; + }, + onExit: (_) { + _isHoveringClaimsMenu = false; + _scheduleCloseClaimsMenu(); + }, + child: Material( + color: Colors.transparent, + child: Container( + width: 120, + padding: const EdgeInsets.symmetric(vertical: 6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildClaimsMenuItem('EB', 'ClaimsPolicies'), + _buildClaimsMenuItem('Non-EB', 'nonEBClaimsList'), + ], + ), + ), + ), + ), + ), + ); + Overlay.of(context).insert(_claimsOverlayEntry!); + } + + Widget _buildClaimsMenuItem(String label, String route) { + return _ClaimsSubMenuItem( + label: label, + onTap: () { + _claimsMenuSuppressUntil = + DateTime.now().add(const Duration(milliseconds: 700)); + _isHoveringClaimsItem = false; + _isHoveringClaimsMenu = false; + _closeClaimsMenu(); + _navigate(route); + }, + ); + } + + void _scheduleCloseClaimsMenu() { + Future.delayed(const Duration(milliseconds: 120), () { + if (!_isHoveringClaimsItem && !_isHoveringClaimsMenu) { + _closeClaimsMenu(); + } + }); + } + + void _closeClaimsMenu() { + _claimsOverlayEntry?.remove(); + _claimsOverlayEntry = null; + _isHoveringClaimsMenu = false; + } + @override Widget build(BuildContext context) { return Container( @@ -198,7 +289,13 @@ class _NhanceSideBarState extends State { // ), ...sideMenuItems.map((item) { + final isClaims = item['route'] == 'ClaimsPolicies'; + final isClaimsActive = + activeRoute == 'ClaimsPolicies' || activeRoute == 'nonEBClaimsList'; + final itemKey = GlobalKey(); + return _SideItem( + key: itemKey, icon: SvgPicture.string( SvgService.getSvg(item['icon']), width: 35, @@ -209,10 +306,22 @@ class _NhanceSideBarState extends State { ), ), label: item['label'], - isActive: activeRoute == item['route'], - onTap: () => _navigate(item['route']), + isActive: isClaims ? isClaimsActive : activeRoute == item['route'], + onTap: isClaims ? null : () => _navigate(item['route']), + onHoverEnter: isClaims + ? () { + _isHoveringClaimsItem = true; + _showClaimsSubMenu(itemKey); + } + : null, + onHoverExit: isClaims + ? () { + _isHoveringClaimsItem = false; + _scheduleCloseClaimsMenu(); + } + : null, ); - }).toList(), + }), if (postModules.isNotEmpty && postModules.contains(5)) _SideItem( @@ -260,42 +369,96 @@ class _SideItem extends StatelessWidget { final String label; final bool isActive; final VoidCallback? onTap; + final VoidCallback? onHoverEnter; + final VoidCallback? onHoverExit; const _SideItem({ + super.key, required this.icon, required this.label, this.isActive = false, this.onTap, + this.onHoverEnter, + this.onHoverExit, }); @override Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Container( - width: double.infinity, - margin: const EdgeInsets.symmetric(vertical: 6), - padding: const EdgeInsets.symmetric(vertical: 10), - decoration: BoxDecoration( - color: isActive - ? const Color(0xFF065D61) // ✅ ACTIVE like your screenshot - : Colors.transparent, - ), - child: Column( - children: [ - // 👇 SVG or Icon widget - icon, - // Icon(icon, color: Colors.white, size: 22), - const SizedBox(height: 6), - Text( - label, - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.w500, + return MouseRegion( + onEnter: (_) => onHoverEnter?.call(), + onExit: (_) => onHoverExit?.call(), + child: InkWell( + onTap: onTap, + child: Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: isActive + ? const Color(0xFF065D61) // ✅ ACTIVE like your screenshot + : Colors.transparent, + ), + child: Column( + children: [ + // 👇 SVG or Icon widget + icon, + // Icon(icon, color: Colors.white, size: 22), + const SizedBox(height: 6), + Text( + label, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, + ), ), - ), - ], + ], + ), + ), + ), + ); + } +} + +class _ClaimsSubMenuItem extends StatefulWidget { + final String label; + final VoidCallback onTap; + + const _ClaimsSubMenuItem({ + required this.label, + required this.onTap, + }); + + @override + State<_ClaimsSubMenuItem> createState() => _ClaimsSubMenuItemState(); +} + +class _ClaimsSubMenuItemState extends State<_ClaimsSubMenuItem> { + bool isHovered = false; + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => setState(() => isHovered = true), + onExit: (_) => setState(() => isHovered = false), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: widget.onTap, + child: Container( + width: double.infinity, + color: isHovered ? const Color(0xFFE5F6F6) : Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + widget.label, + style: TextStyle( + fontSize: 12, + color: isHovered ? const Color(0xFF006F73) : Colors.black87, + fontWeight: isHovered ? FontWeight.w600 : FontWeight.w400, + ), + ), + ), ), ), ); diff --git a/lib/main.dart b/lib/main.dart index 325caa8..2334248 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,7 @@ import 'package:nhancepolicy/presentation/excelVerification.dart'; import 'package:nhancepolicy/presentation/postFileUpload.dart'; import 'package:nhancepolicy/presentation/cdList.dart'; import 'package:nhancepolicy/presentation/claims.dart'; +import 'package:nhancepolicy/presentation/nonEBClaimsList.dart'; import 'package:nhancepolicy/presentation/policies.dart'; import 'package:nhancepolicy/service/session/session_service.dart'; import 'package:nhancepolicy/service/token_storage_service.dart'; @@ -297,6 +298,7 @@ final Map appRoutes = { 'ClaimsPolicies': (context) => ClaimsPolicies( empCode: '', ), + 'nonEBClaimsList': (context) => const NonEBClaimsList(empCode: '',), 'cdTransactionDetails': (context) => cdTransactionDetails( insurerName: '', cdMasterAccountNo: '', diff --git a/lib/presentation/claims.dart b/lib/presentation/claims.dart index fc36f5d..f6ec3bc 100755 --- a/lib/presentation/claims.dart +++ b/lib/presentation/claims.dart @@ -156,7 +156,7 @@ class _ClaimsPolicieState extends State { _postPreToken = tokenService.getCurrentToken(); empClientId = await tokenService.readValue('empClientId'); empClientBranchId = await tokenService.readValue('empClientBranchId'); - + logDebug('empClientId0000 $empClientId'); getClaimsPoliciesDetails(); // ✅ If empCode passed → auto filter @@ -188,7 +188,7 @@ class _ClaimsPolicieState extends State { try { logDebug('10'); final response = - await apiService.getClaimPoliciesToApi(_postPreToken!, ''); + await apiService.getClaimPoliciesToApi(_postPreToken!, empClientId); if (response['status'] == 'success') { setState(() { isLoading = false; diff --git a/lib/presentation/claimshistory.dart b/lib/presentation/claimshistory.dart index c3f600f..9b3c950 100755 --- a/lib/presentation/claimshistory.dart +++ b/lib/presentation/claimshistory.dart @@ -661,13 +661,11 @@ class _ClaimHistoryPopupState extends State crossAxisAlignment: CrossAxisAlignment.start, children: List.generate(stepKeys.length, (index) { String stepTitleKey = stepKeys[index]; - Map stepData = - stepMap[stepTitleKey]; + Map stepData = stepMap[stepTitleKey]; Widget content = _getStepContentFromApi(stepData); return _buildStep( stepNumber: index + 1, - title: - _getStepTitleFromApi(stepTitleKey, stepData), + title: _getStepTitleFromApi(stepTitleKey, stepData), content: content, isLast: index == stepKeys.length - 1, ); diff --git a/lib/presentation/hrPolicyDetails.dart b/lib/presentation/hrPolicyDetails.dart index 5274af5..8c27025 100755 --- a/lib/presentation/hrPolicyDetails.dart +++ b/lib/presentation/hrPolicyDetails.dart @@ -131,9 +131,13 @@ class _HrPolicyDetailsState extends State List currentPageIds = []; List get _paginatedData { - final startIndex = (_currentPage - 1) * _rowsPerPage; - final endIndex = - (_currentPage * _rowsPerPage).clamp(0, filteredData.length); + final total = filteredData.length; + if (total == 0) return []; + + final startIndex = ((_currentPage - 1) * _rowsPerPage).clamp(0, total); + final endIndex = (_currentPage * _rowsPerPage).clamp(0, total); + + if (startIndex >= endIndex) return []; return filteredData.sublist(startIndex, endIndex); } @@ -456,10 +460,12 @@ class _HrPolicyDetailsState extends State if (lowerQuery.isEmpty) { setState(() { + _currentPage = 1; filteredData = List.from(originalData); }); } else { setState(() { + _currentPage = 1; filteredData = originalData.where((row) { final status = row['status']?.toString().toLowerCase().trim() ?? ''; @@ -475,6 +481,11 @@ class _HrPolicyDetailsState extends State return row['name']?.toString().toLowerCase().contains(lowerQuery) == true || + row['emp_code'] + ?.toString() + .toLowerCase() + .contains(lowerQuery) == + true || row['uhid']?.toString().toLowerCase().contains(lowerQuery) == true || row['relationship'] diff --git a/lib/presentation/nonEBClaimsCreate.dart b/lib/presentation/nonEBClaimsCreate.dart new file mode 100644 index 0000000..ec8ec75 --- /dev/null +++ b/lib/presentation/nonEBClaimsCreate.dart @@ -0,0 +1,777 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:file_picker/file_picker.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:intl/intl.dart'; +import 'package:nhancepolicy/logger.dart'; +import 'package:pdf/widgets.dart' as pw; + +import '../config/environment.dart'; +import '../customAppBar/toastHelper.dart'; +import '../service/api_service.dart'; +import '../service/token_storage_service.dart'; + +class NonEBClaimsCreate extends StatefulWidget { + final BuildContext parentContext; + final VoidCallback onSuccess; + + const NonEBClaimsCreate({ + Key? key, + required this.parentContext, + required this.onSuccess, + }) : super(key: key); + + @override + State createState() => _NonEBClaimsCreateState(); +} + +class _NonEBClaimsCreateState extends State { + bool isLoading = false; + dynamic empClientId; + dynamic empClientBranchId; + final tokenService = TokenStorageService(); + String? _postPreToken = ''; + List> policyNumberList = []; + int? policyNumberId; + bool isSubmitting = false; + Map getClaimPoliciesApi = {}; + int? selectedClientPolicyId; + bool isPolicyValid = true; + bool isNatureOfLossValid = true; + bool isLossLocationValid = true; + bool isLossDateValid = true; + bool isLossDescriptionValid = true; + + final TextEditingController natureOfLossController = TextEditingController(); + final TextEditingController lossLocationController = TextEditingController(); + final TextEditingController lossDescriptionController = + TextEditingController(); + DateTime? lossDate; + PlatformFile? selectedAssetFile; + late ApiService apiService; + + @override + void initState() { + super.initState(); + apiService = ApiService(context); + _loadToken(); + } + + @override + void dispose() { + natureOfLossController.dispose(); + lossLocationController.dispose(); + lossDescriptionController.dispose(); + super.dispose(); + } + + Future _loadToken() async { + _postPreToken = tokenService.getCurrentToken(); + empClientId = await tokenService.readValue('empClientId'); + empClientBranchId = await tokenService.readValue('empClientBranchId'); + getClaimsPoliciesDetails(); + } + + Future getClaimsPoliciesDetails() async { + logDebug('9'); + setState(() { + isLoading = true; + }); + try { + logDebug('10'); + + final request = { + "client_id": empClientId, + "client_branch_id": empClientBranchId + }; + + final response = + await apiService.getNonEBClaimPoliciesToApi(_postPreToken!, request); + if (response['status'] == 'success' || response['status'] == true) { + setState(() { + isLoading = false; + final data = response['data']; + final rawList = data is List ? data : []; + + policyNumberList = rawList.map>((policy) { + final map = Map.from(policy as Map); + return { + // Send this id as client_policy_id in create API payload. + 'id': int.tryParse(map['id'].toString()) ?? 0, + // Show only policy number in dropdown. + 'label': (map['policy_no'] ?? '').toString(), + }; + }).where((p) => p['id'] != 0).toList(); + }); + } 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 sendFormDataToApi() async { + setState(() { + isPolicyValid = selectedClientPolicyId != null; + isNatureOfLossValid = natureOfLossController.text.trim().isNotEmpty; + isLossLocationValid = lossLocationController.text.trim().isNotEmpty; + isLossDateValid = lossDate != null; + isLossDescriptionValid = lossDescriptionController.text.trim().isNotEmpty; + }); + + if (!isPolicyValid || + !isNatureOfLossValid || + !isLossLocationValid || + !isLossDateValid || + !isLossDescriptionValid) { + ToastHelper.showErrorToast( + context, + 'Please Fill Required Fields', + ); + return; + } + + if (selectedAssetFile == null || selectedAssetFile!.bytes == null) { + ToastHelper.showErrorToast( + context, + 'Please upload one document', + ); + return; + } + + setState(() => isSubmitting = true); // 🔥 start loader + setState(() { + isLoading = true; + }); + + try { + logDebug('Check One'); + final fields = { + 'client_policy_id': selectedClientPolicyId, + 'nature_of_loss': natureOfLossController.text.trim(), + 'loss_location': lossLocationController.text.trim(), + 'loss_date': DateFormat('dd-MM-yyyy').format(lossDate!), + 'loss_description': lossDescriptionController.text.trim(), + }; + + final request = http.MultipartRequest( + 'POST', Uri.parse('${Environment.apiUrlPost}api/v1/non-eb-claim/create')); + request.headers['Authorization'] = 'Bearer $_postPreToken'; + request.headers['APP-SIGNATURE'] = + 'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y'; + final stringFields = + fields.map((key, value) => MapEntry(key, value?.toString() ?? '')); + logDebug("📁 stringFields: ${stringFields}"); + request.fields.addAll(stringFields); + + final pf = selectedAssetFile!; + final ext = pf.extension?.toLowerCase() ?? ''; + Uint8List fileBytes = pf.bytes!; + var fileName = pf.name; + + if (['jpg', 'jpeg', 'png', 'heic'].contains(ext)) { + final pdf = pw.Document(); + final image = pw.MemoryImage(fileBytes); + pdf.addPage( + pw.Page( + build: (pw.Context context) => pw.Center( + child: pw.Image(image, fit: pw.BoxFit.contain), + ), + ), + ); + fileBytes = await pdf.save(); + fileName = pf.name.replaceAll(RegExp(r'\.\w+$'), '.pdf'); + } + + request.files.add(http.MultipartFile.fromBytes( + 'asset_file', + fileBytes, + filename: fileName, + )); + + logDebug("Payload being sent:"); + logDebug("File: ${pf.name}"); + + final response = await request.send(); + final responseBody = await response.stream.bytesToString(); + + final decoded = jsonDecode(responseBody); + if (decoded['status'] == true) { + resetFormOnServiceChange(); + Navigator.pop(context); + widget.onSuccess(); + + ToastHelper.showSuccessToast(context, decoded['message']); + setState(() { + isLoading = false; + }); + logDebug('Form data submitted successfully'); + } else { + setState(() { + isLoading = false; + }); + Navigator.pop(context); + ToastHelper.showErrorToast(context, "Failed: ${decoded['message']}"); + } + } catch (e) { + setState(() { + isLoading = false; + }); + logDebug('Error submitting form data: $e'); + } finally { + setState(() => isSubmitting = false); // 🔥 stop loader + } + } + + void resetFormOnServiceChange() { + selectedClientPolicyId = null; + policyNumberId = null; + natureOfLossController.clear(); + lossLocationController.clear(); + lossDescriptionController.clear(); + lossDate = null; + selectedAssetFile = null; + + isPolicyValid = true; + isNatureOfLossValid = true; + isLossLocationValid = true; + isLossDateValid = true; + isLossDescriptionValid = true; + + } + + Future pickSingleAssetFile() async { + final result = await FilePicker.platform.pickFiles( + withData: true, + allowMultiple: false, + ); + + if (result != null && result.files.isNotEmpty) { + setState(() { + selectedAssetFile = result.files.first; + }); + } + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + resetFormOnServiceChange(); + return true; + }, + child: Dialog( + backgroundColor: Colors.white, // ✅ PURE WHITE popup + insetPadding: const EdgeInsets.all(20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: SizedBox( + width: MediaQuery.of(context).size.width * + 0.75, // Desktop popup width + // height: MediaQuery.of(context).size.height * 0.85, + child: Stack( + children: [ + /// MAIN CONTENT (YOUR EXISTING UI) + SingleChildScrollView( + child: Container( + padding: EdgeInsets.all(20), + color: Colors.white, + child: Column( + children: [ + /// 🔹 HEADER ROW (Title + Close) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Raise Insurance Claim", + style: GoogleFonts.poppins( + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => { + setState(() { + resetFormOnServiceChange(); + }), + Navigator.pop(context) + }, + ), + ], + ), + + const SizedBox(height: 16), + + Column( + children: [ + _row([ + buildDropdownField( + 'Select Policy', + (value) { + setState(() { + policyNumberId = value; + selectedClientPolicyId = value; + isPolicyValid = true; + }); + }, + policyNumberList, + 'label', + policyNumberId, + required: true, + isValid: isPolicyValid, + ), + buildTextField( + 'Nature Of Loss', + natureOfLossController, + required: true, + isValid: isNatureOfLossValid, + ), + ]), + _row([ + buildTextField( + 'Loss Location', + lossLocationController, + required: true, + isValid: isLossLocationValid, + ), + buildDatePickerField( + label: 'Loss Date', + selectedDate: lossDate, + allowFuture: false, + onDateSelected: (d) => + setState(() => lossDate = d), + required: true, + isValid: isLossDateValid, + ), + ]), + _row([ + buildTextAreaField( + 'Loss Description', + lossDescriptionController, + required: true, + isValid: isLossDescriptionValid, + ), + ]), + _row([ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + fieldLabel( + 'Upload Document', + required: true, + ), + Container( + height: 42, + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + decoration: BoxDecoration( + color: const Color(0xFFF1F1F1), + borderRadius: + BorderRadius.circular(8), + ), + child: Row( + children: [ + Expanded( + child: Text( + selectedAssetFile?.name ?? + 'Choose one file', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, + ), + ), + ), + TextButton( + onPressed: pickSingleAssetFile, + child: const Text('Browse'), + ), + if (selectedAssetFile != null) + IconButton( + onPressed: () { + setState(() { + selectedAssetFile = null; + }); + }, + icon: const Icon( + Icons.close, + size: 18, + ), + ), + ], + ), + ), + if (selectedAssetFile == null) + const SizedBox( + height: 16, + child: Text( + "Required", + style: TextStyle( + color: Colors.red, + fontSize: 11, + ), + ), + ) + else + const SizedBox(height: 16), + ], + ), + ]), + Align( + alignment: Alignment.centerRight, + child: SizedBox( + width: 120, + height: 42, + child: ElevatedButton( + onPressed: isSubmitting + ? null + : sendFormDataToApi, + style: ElevatedButton.styleFrom( + backgroundColor: + const Color(0xFFE26728), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(10), + ), + ), + child: isSubmitting + ? const SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( + 'Send', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + + /// 🔥 LOADER OVERLAY + if (isLoading) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.6), + borderRadius: BorderRadius.circular(20), + ), + child: Center( + child: Image.asset( + 'assets/nhance-loader.gif', + height: 60, + width: 60, + ), + ), + ), + ), + ], + ), + ), + ))); + } + + /// ---------- HELPERS ---------- + + Widget _row(List children) { + return Padding( + padding: const EdgeInsets.only(bottom: 14), + child: Row( + children: children + .map((e) => Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: e, + ))) + .toList(), + ), + ); + } + + Widget fieldLabel(String text, {bool required = false}) { + return Padding( + padding: const EdgeInsets.only(bottom: 6), + child: RichText( + text: TextSpan( + text: text, + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + children: required + ? const [ + TextSpan( + text: ' *', + style: TextStyle(color: Colors.red), + ) + ] + : [], + ), + ), + ); + } + + Widget errorText(bool isValid) { + return SizedBox( + height: 16, // fixed height for alignment + child: isValid + ? null + : const Text( + "Required", + style: TextStyle( + color: Colors.red, + fontSize: 11, + ), + ), + ); + } + + Widget buildTextField( + String label, + TextEditingController controller, { + bool required = false, + bool isValid = true, + TextInputType keyboardType = TextInputType.text, + List? inputFormatters, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + fieldLabel(label, required: required), + formBox( + child: TextField( + controller: controller, + keyboardType: keyboardType, + inputFormatters: inputFormatters, + decoration: const InputDecoration( + isDense: true, + border: InputBorder.none, + ), + ), + ), + errorText(isValid), + ], + ); + } + + Widget buildTextAreaField( + String label, + TextEditingController controller, { + bool required = false, + bool isValid = true, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + fieldLabel(label, required: required), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF1F1F1), + borderRadius: BorderRadius.circular(8), + ), + child: TextField( + controller: controller, + maxLines: 3, + decoration: const InputDecoration( + isDense: true, + border: InputBorder.none, + ), + ), + ), + errorText(isValid), + ], + ); + } + + Widget buildDropdownField( + String label, + void Function(int?) onChanged, + List> itemsList, + String displayField, + int? selectedValue, { + bool required = false, + bool isValid = true, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + fieldLabel(label, required: required), + formBox( + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: selectedValue, + hint: const Text('Select'), + icon: const Icon(Icons.keyboard_arrow_down), + + /// ✅ This controls selected value (closed state) + selectedItemBuilder: (context) { + return itemsList.map((item) { + return Align( + alignment: Alignment.centerLeft, + child: Text( + item[displayField] ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + ), + ); + }).toList(); + }, + + /// ✅ This controls dropdown list (open state) + items: itemsList.map>((item) { + return DropdownMenuItem( + value: item['id'], + child: Text( + item[displayField] ?? '', + style: const TextStyle(fontSize: 13), + ), // FULL TEXT here + ); + }).toList(), + + onChanged: onChanged, + ), + ), + ), + if (required) errorText(isValid), + ], + ); + } + + Widget buildDatePickerField({ + required String label, + required DateTime? selectedDate, + required bool allowFuture, + required ValueChanged onDateSelected, + DateTime? minDate, + DateTime? maxDate, + bool required = false, + bool isValid = true, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + fieldLabel(label, required: required), + Container( + height: 42, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: const Color(0xFFF1F1F1), + borderRadius: BorderRadius.circular(8), + // border: required && !isValid + // ? Border.all(color: Colors.red) + // : null, + ), + child: InkWell( + onTap: () async { + final DateTime now = DateTime.now(); + final DateTime first = minDate ?? DateTime(1980); + final DateTime last = + allowFuture ? (maxDate ?? DateTime(2100)) : now; + + final DateTime initialDate = + selectedDate ?? (first.isAfter(now) ? first : now); + + final picked = await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: first, + lastDate: last, + initialEntryMode: DatePickerEntryMode.calendarOnly, + ); + + if (picked != null) { + onDateSelected(picked); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + selectedDate != null + ? DateFormat('dd-MM-yyyy').format(selectedDate) + : 'Select', + style: const TextStyle(color: Colors.black), + ), + // ✅ Show clear button only if date selected + if (selectedDate != null) + GestureDetector( + onTap: () { + onDateSelected(null); // 🔥 Clear date + }, + child: const Icon( + Icons.close, + size: 18, + color: Colors.grey, + ), + ) + else + const Icon(Icons.calendar_today, size: 18), + ], + ), + ), + ), + errorText(isValid), + ], + ); + } + + Widget formBox({required Widget child}) { + return Container( + height: 42, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: const Color(0xFFF1F1F1), // 👈 light grey + borderRadius: BorderRadius.circular(8), + ), + child: Center(child: child), + ); + } +} + +/// ---------- STYLES ---------- +final _labelStyle = GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w500, +); diff --git a/lib/presentation/nonEBClaimsHistory.dart b/lib/presentation/nonEBClaimsHistory.dart new file mode 100644 index 0000000..46479f1 --- /dev/null +++ b/lib/presentation/nonEBClaimsHistory.dart @@ -0,0 +1,1642 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:http/http.dart' as http; +import 'package:nhancepolicy/responsive.dart'; +import 'package:nhancepolicy/service/api_service.dart'; +import 'package:nhancepolicy/service/file_upload_service.dart'; +import 'package:nhancepolicy/service/multi_file_upload_widget.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:pdf/widgets.dart' as pw; +import '../../../config/environment.dart'; +import '../../../customAppBar/toastHelper.dart'; +import 'package:nhancepolicy/logger.dart'; + +class NonEBClaimsHistory extends StatefulWidget { + final String ticket_id; + final String empName; + final String policyType; + final String? clientPolicyNo; + final String? claimNo; + final String postToken; + + const NonEBClaimsHistory({ + Key? key, + required this.ticket_id, + required this.empName, + required this.policyType, + this.clientPolicyNo, + this.claimNo, + required this.postToken, + }) : super(key: key); + + @override + State createState() => _NonEBClaimsHistoryState(); +} + +class _NonEBClaimsHistoryState extends State + with TickerProviderStateMixin { + late ApiService apiService; + List> getClaimsHistoryList = []; + List stepKeys = []; + late Map stepMap; + bool isLoading = false; + dynamic _token; + + String? selectedFileNames; + // html.File? uploadedFile; + // List uploadedFiles = []; + List uploadedFiles = []; + final FileUploadService fileService = FileUploadService(); + + List claimFiles = []; + List> requiredDocsList = []; + List> requiredDocsListBackup = []; + bool isSubmitting = false; + + bool isActionFreeze = false; + + // IR Docs state + bool showIRDocs = false; + // final List> irDocList = [ + // {"title": "Hospital Bill", "checked": false}, + // {"title": "Discharge Summary", "checked": false}, + // {"title": "Prescription", "checked": false}, + // ]; + final Map _assignedFiles = {}; + + @override + void initState() { + super.initState(); + apiService = ApiService(context); + + // debug prints kept + logDebug("CLAIMHISTORY"); + logDebug(widget.claimNo); + logDebug(widget.clientPolicyNo); + logDebug(widget.policyType); + logDebug(widget.ticket_id); + getClaimsHistoryDetails(); + } + + @override + void dispose() { + super.dispose(); + } + + // ------------------------- + // File pick (web + mobile) + // ------------------------- + Future pickFile() async { + final result = await FilePicker.platform + .pickFiles(withData: true, allowMultiple: false); + if (result != null && result.files.isNotEmpty) { + return result.files.first; + } + return null; + } + + // ------------------------- + // Assign same picked file to all checked docs + // ------------------------- + Future handleUploadForSelected() async { + final checked = + requiredDocsList.where((d) => d['document_received'] == true).toList(); + if (checked.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Please select at least one document"))); + return; + } + + final picked = await pickFile(); + if (picked == null) return; + + setState(() { + for (var d in checked) { + final title = d['document_name'] as String; + _assignedFiles[title] = picked; + } + }); + } + + // ------------------------- + // Remove assignment + // ------------------------- + void removeAssignedFile(String title) { + setState(() { + _assignedFiles[title] = null; + }); + } + + // ------------------------- + // Submit IR Docs (placeholder) + // Replace with real API upload logic + // ------------------------- + Future submitIRDocs() async { + setState(() => isSubmitting = true); // 🔥 start loader + + try { + logDebug('enter'); + + // STEP 1: Must select at least one document type + final selectedDocs = requiredDocsList + .where((d) => d['document_received'] == true) + .toList(); + if (selectedDocs.isEmpty) { + ToastHelper.showErrorToast( + context, "Please select at least one IR document type"); + return; + } + + // STEP 2: Must upload at least one file + if (!MultiFileUploadWidget.hasFiles || fileService.files.isEmpty) { + ToastHelper.showErrorToast( + context, "Please upload at least one document"); + return; + } + + // STEP 3: All uploaded files must have a label + for (final uf in fileService.files) { + if ((uf.label ?? "").trim().isEmpty) { + ToastHelper.showErrorToast( + context, "Please enter name for all uploaded documents"); + return; + } + } + + // STEP 4: Prepare multipart request + final url = Uri.parse("${Environment.apiUrlPost}api/v1/non-eb-claim/upload-required-doc"); + final request = http.MultipartRequest('POST', url); + + request.headers['Authorization'] = "Bearer ${widget.postToken}"; + request.headers['APP-SIGNATURE'] = + 'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y'; + + final requiredDocsPayload = { + "is_action_freeze": isActionFreeze, + "docs": requiredDocsList.map((d) { + return { + "document_name": d["document_name"], + "document_received": d["document_received"], + }; + }).toList(), + }; + + request.fields["required_docs"] = jsonEncode(requiredDocsPayload); + + // Add mapped fields if required + request.fields['ticket_id'] = widget.ticket_id; + + // STEP 5: Convert image → PDF and attach files + for (var uf in fileService.files) { + final pf = uf.file; + final ext = pf.extension?.toLowerCase() ?? ""; + + Uint8List fileBytes = pf.bytes!; + + // if (['jpg', 'jpeg', 'png', 'heic'].contains(ext)) { + // final pdf = pw.Document(); + // final image = pw.MemoryImage(fileBytes); + // + // pdf.addPage( + // pw.Page( + // build: (context) => pw.Center(child: pw.Image(image)), + // ), + // ); + // + // fileBytes = await pdf.save(); + // + // final pdfName = pf.name.replaceAll(RegExp(r'\.\w+$'), '.pdf'); + // + // request.files.add(http.MultipartFile.fromBytes( + // "claim_docs[]", + // fileBytes, + // filename: pdfName, + // )); + // } else { + request.files.add(http.MultipartFile.fromBytes( + "claim_docs[]", + fileBytes, + filename: pf.name, + )); + // } + } + + // STEP 6: Add names for these IR docs + final labels = fileService.files.map((f) => f.label.trim()).toList(); + request.fields['claim_doc_names'] = jsonEncode(labels); + + // Debug + logDebug( + "Files uploaded: ${fileService.files.map((e) => e.file.name).toList()}"); + logDebug("Labels: $labels"); + final stringFields = request.fields.map((key, value) => MapEntry(key, value?.toString() ?? '')); + logDebug("📁 stringFields: ${stringFields}"); + + // STEP 7: Send the request + final response = await request.send(); + final responseBody = await response.stream.bytesToString(); + final decoded = jsonDecode(responseBody); + + if (decoded["status"] == true) { + ToastHelper.showSuccessToast(context, decoded["message"]); + + _resetIRDocs(); + await getClaimsHistoryDetails(); // <--- refresh checkbox state from API + setState(() => showIRDocs = false); + } else { + ToastHelper.showErrorToast( + context, "Upload failed: ${decoded['message']}"); + } + } catch (e) { + ToastHelper.showErrorToast(context, "Upload failed"); + } finally { + setState(() => isSubmitting = false); // 🔥 stop loader + } + } + + // ------------------------- + // Existing API call (unchanged) + // ------------------------- + Future getClaimsHistoryDetails() async { + setState(() { + isLoading = true; + }); + try { + final response = await apiService.getNonEBClaimsHistoryToApi( + widget.ticket_id, widget.postToken); + if (response['status'] == 'success' || response['status'] == true) { + setState(() { + isLoading = false; + // New response shape support: + // { + // claims_data: {...}, + // required_docs: {...}, + // claims_files: [...], + // ticket_data: [{status, changed_at}] + // } + // Also keep fallback for previous nested data response. + final root = Map.from(response); + final nestedData = root['data'] is Map + ? Map.from(root['data']) + : {}; + + final ticketDataRaw = root['ticket_data'] ?? nestedData['ticket_data']; + if (ticketDataRaw is List) { + getClaimsHistoryList = ticketDataRaw + .map>((e) => Map.from(e)) + .toList(); + } else if (ticketDataRaw is Map) { + getClaimsHistoryList = [Map.from(ticketDataRaw)]; + } else { + getClaimsHistoryList = []; + } + + stepMap = getClaimsHistoryList.isNotEmpty + ? getClaimsHistoryList.first + : {}; + stepKeys = List.generate(getClaimsHistoryList.length, (i) => '$i'); + + final claimsDocs = root['claims_files'] ?? + root['claim_files'] ?? + nestedData['claims_files'] ?? + nestedData['claim_files']; + claimFiles = claimsDocs is List + ? List>.from(claimsDocs) + : >[]; + + final requiredDocsRoot = + root['required_docs'] ?? nestedData['required_docs']; + if (requiredDocsRoot is Map) { + isActionFreeze = requiredDocsRoot['is_action_freeze'] ?? false; + final requiredDocs = requiredDocsRoot['docs']; + requiredDocsList = requiredDocs is List + ? List>.from(requiredDocs) + : >[]; + } else { + isActionFreeze = false; + requiredDocsList = >[]; + } + +// ⭐ Make a backup copy to restore later + requiredDocsListBackup = requiredDocsList + .map((doc) => { + "document_name": doc["document_name"], + "document_received": doc["document_received"], + }) + .toList(); + + logDebug(isActionFreeze); + logDebug('requiredDocsList $requiredDocsList'); + + for (var d in requiredDocsList) { + _assignedFiles[d['document_name']] = null; + } + }); + } else { + setState(() => isLoading = false); + logDebug('Request failed with status: ${response['code']}'); + } + } catch (e) { + setState(() => isLoading = false); + logDebug('Exception occurred: $e'); + } + } + + Future _launchURL(String url) async { + final Uri uri = Uri.parse(url); + try { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } catch (e) { + logDebug('Could not launch URL: $e'); + } + } + + void _resetIRDocs() { + // Restore checkbox values from API backup + requiredDocsList = requiredDocsListBackup + .map((doc) => { + "document_name": doc["document_name"], + "document_received": doc["document_received"], + }) + .toList(); + + // Clear assigned uploaded files + _assignedFiles.updateAll((key, value) => null); + + // Reset multi-file upload widget service + fileService.clearAll(); + MultiFileUploadWidget.hasFiles = false; + + setState(() {}); + } + + // ------------------------- + // Build + // ------------------------- + @override + Widget build(BuildContext context) { + // responsive decisions + final mediaW = MediaQuery.of(context).size.width; + final isMobile = mediaW < 600; + final panelWidth = + isMobile ? MediaQuery.of(context).size.width * 0.95 : 400.0; + + final keyValueWidgets = [ + _buildKeyValue( + 'Name', '${widget.empName ?? ''}'), + SizedBox( + width: Responsive.isDesktop(context) ? 16 : 0, + height: Responsive.isDesktop(context) ? 0 : 8), + _buildKeyValue('Policy Name', + '${widget.policyType ?? ''} - ${widget.clientPolicyNo ?? ''}'), + ]; + + final keyValueWidgetRow = [ + // _buildKeyValue( + // 'Claims Amount', + // widget.claimAmount == null || widget.claimAmount!.trim().isEmpty + // ? 'N/A' + // : '₹${widget.claimAmount}'), + // SizedBox( + // width: Responsive.isDesktop(context) ? 16 : 0, + // height: Responsive.isDesktop(context) ? 0 : 8), + _buildKeyValue( + 'Claims Number', + widget.claimNo == null || widget.claimNo!.trim().isEmpty + ? 'N/A' + : widget.claimNo!), + ]; + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (didPop) return; + Navigator.pop(context); + }, + child: Container( + // keep popup sized; adjust if required + constraints: BoxConstraints(maxWidth: 1080, maxHeight: 820), + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, borderRadius: BorderRadius.circular(12)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header row (keeps IR docs icon + close inside popup header) + Row( + children: [ + Expanded( + child: Text( + 'Claims History', + style: GoogleFonts.poppins( + fontSize: Responsive.isDesktop(context) ? 20 : 16, + fontWeight: FontWeight.w500, + color: Color(0xFF101010), + ), + ), + ), + + // IR Docs icon + if (!isActionFreeze && requiredDocsList.isNotEmpty) + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + setState(() { + showIRDocs = true; + }); + }, + child: Container( + height: 34, + width: 34, + margin: EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Color(0xFFBCBCBC)), + borderRadius: BorderRadius.circular(6), + ), + child: Icon(Icons.folder_open, + size: 20, color: Color(0xFF00A5A8)), + ), + ), + ), + + // Close popup + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + height: 30, + width: 30, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Color(0xFFBCBCBC)), + borderRadius: BorderRadius.circular(6), + ), + child: + Icon(Icons.close, size: 25, color: Color(0xFFBCBCBC)), + ), + ), + ) + ], + ), + + SizedBox(height: Responsive.isDesktop(context) ? 10 : 6), + + // BODY: for mobile we will show either the Claim content or full IR Docs content (switch) + // for desktop we show Row with left content and optional right panel + // Expanded( child: isMobile ? AnimatedSwitcher( duration: Duration(milliseconds: 300), transitionBuilder: (child, animation) { final offsetAnimation = Tween( begin: Offset(0, 1), end: Offset(0, 0)) .animate(animation); return SlideTransition( position: offsetAnimation, child: child); }, child: showIRDocs ? _buildMobileFullIrDocs(panelWidth, key: ValueKey('mobile_ir')) : _buildMainLeftContent(key: ValueKey('main_left')), ) : Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // LEFT: main content - scrollable Expanded(child: _buildMainLeftContent()), // RIGHT: IR panel - desktop inline (only visible on wide screens) AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, width: showIRDocs ? panelWidth : 0, child: showIRDocs ? _buildIrDocsPanel(panelWidth, isMobile: false) : const SizedBox.shrink(), ), ], ), ), + Expanded( + child: isMobile + ? AnimatedSwitcher( + duration: Duration(milliseconds: 300), + transitionBuilder: (child, animation) { + final offsetAnimation = Tween( + begin: Offset(0, 1), end: Offset(0, 0)) + .animate(animation); + return SlideTransition( + position: offsetAnimation, child: child); + }, + child: showIRDocs + ? buildIRDocsContent( + isDesktop: false, + key: ValueKey('mobile_ir'), + ) + : _buildMainLeftContent( + key: ValueKey('main_left'), + showIRDocs: showIRDocs), + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // LEFT: main content - scrollable + Expanded( + child: + _buildMainLeftContent(showIRDocs: showIRDocs)), + + // RIGHT: IR panel - desktop inline (only visible on wide screens) + AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + width: showIRDocs ? panelWidth : 0, + child: showIRDocs + ? buildIRDocsContent( + isDesktop: true, + panelWidth: panelWidth, + key: ValueKey('desktop_ir'), + ) + : SizedBox.shrink(), + ), + ], + ), + ), + ], + ), + ), + ); + } + + // ------------------------- + // Main left content extracted to keep code tidy + // ------------------------- + Widget _buildMainLeftContent({ + Key? key, + required bool showIRDocs, + }) { + return SingleChildScrollView( + key: key, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Claim Details card (unchanged) + Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: const Color(0xFFEBEBEB), + blurRadius: 14, + spreadRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// ----------------------------------------- + /// NAME + POLICY NAME SECTION + /// ----------------------------------------- + Responsive.isDesktop(context) + ? Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildKeyValue( + 'Name', + '${widget.empName ?? ''}', + ), + ), + const SizedBox(width: 24), + Expanded( + child: _buildKeyValue( + 'Policy Name', + widget.clientPolicyNo == null || + widget.clientPolicyNo!.trim().isEmpty + ? (widget.policyType ?? '') + : '${widget.policyType ?? ''} - ${widget.clientPolicyNo}', + ), + ), + + // Expanded( + // child: _buildKeyValue( + // 'Policy Name', + // '${widget.policyType ?? ''} - ${widget.clientPolicyNo ?? ''}', + // ), + // ), + ], + ) + : SizedBox( + width: double.infinity, // ⬅️ forces full width + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildKeyValue( + 'Name', + '${widget.empName ?? ''}', + ), + const SizedBox(height: 12), + _buildKeyValue( + 'Policy Name', + '${widget.policyType ?? ''} - ${widget.clientPolicyNo ?? ''}', + ), + ], + ), + ), + + const SizedBox(height: 24), + + /// ----------------------------------------- + /// CLAIM AMOUNT + CLAIM NUMBER SECTION + /// ----------------------------------------- + Responsive.isDesktop(context) + ? Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Expanded( + // child: _buildKeyValue( + // 'Claims Amount', + // (widget.claimAmount == null || + // widget.claimAmount!.trim().isEmpty) + // ? 'N/A' + // : '₹${widget.claimAmount}', + // ), + // ), + // const SizedBox(width: 24), + Expanded( + child: _buildKeyValue( + 'Claims Number', + (widget.claimNo == null || + widget.claimNo!.trim().isEmpty) + ? 'N/A' + : widget.claimNo!, + ), + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // _buildKeyValue( + // 'Claims Amount', + // (widget.claimAmount == null || + // widget.claimAmount!.trim().isEmpty) + // ? 'N/A' + // : '₹${widget.claimAmount}', + // ), + // const SizedBox(height: 8), + _buildKeyValue( + 'Claims Number', + (widget.claimNo == null || + widget.claimNo!.trim().isEmpty) + ? 'N/A' + : widget.claimNo!, + ), + ], + ), + ], + ), + ), + + SizedBox(height: 10), + + // Loading or step content + isLoading + ? Container( + child: Center( + child: Image.asset( + height: 60, width: 60, 'assets/nhance-loader.gif'))) + : Container( + child: getClaimsHistoryList.isNotEmpty + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate(getClaimsHistoryList.length, (index) { + final stepData = getClaimsHistoryList[index]; + Widget content = _getStepContentFromApi(stepData); + return _buildStep( + stepNumber: index + 1, + title: _getStepTitleFromApi(stepData), + content: content, + isLast: index == getClaimsHistoryList.length - 1, + ); + }), + ) + : Container( + height: MediaQuery.of(context).size.height * 0.4, + child: Center( + child: Column( + children: [ + Image.asset('assets/searchData.jpg', + width: 200, height: 200, fit: BoxFit.cover), + Text('No Claims History', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 15)), + ], + ), + ), + ), + ), + + // Claim files list + // Container( + // margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + // child: claimFiles.isEmpty + // ? const Center(child: Text("No files available")) + // : SizedBox( + // height: 300, + // child: ListView.builder( + // itemCount: claimFiles.length, + // itemBuilder: (context, index) { + // final file = claimFiles[index]; + // return Card( + // child: ListTile( + // leading: const Icon(Icons.insert_drive_file, + // color: Colors.blue), + // title: Text(file['claim_file_name']), + // trailing: + // const Icon(Icons.download, color: Colors.green), + // onTap: () => _launchURL(file['url']), + // ), + // ); + // }, + // ), + // ), + // ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + // padding: const EdgeInsets.all(16), + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(6), + // boxShadow: [ + // BoxShadow( + // color: Colors.black.withOpacity(0.08), + // blurRadius: 6, + // offset: const Offset(0, 2), + // ), + // ], + // ), + child: claimFiles.isEmpty + ? const Center(child: Text("No files available")) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Submitted Documents', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: claimFiles.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: + showIRDocs ? 2 : 3, // ✅ desktop: 3 per row + crossAxisSpacing: 16, + mainAxisSpacing: 16, + childAspectRatio: 5.9, // controls height + ), + itemBuilder: (context, index) { + final file = claimFiles[index]; + + return InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => _launchURL(file['url']), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + /// FILE ICON + Container( + child: const Icon( + Icons.insert_drive_file, + color: Color(0xFF3B5BDB), + size: 25, + ), + ), + + const SizedBox(width: 10), + + /// FILE NAME + Expanded( + child: Text( + file['claim_file_name'] ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + + const SizedBox(width: 8), + + /// DOWNLOAD ICON + Tooltip( + message: 'Download', // Added tooltip name + child: InkWell( + borderRadius: BorderRadius.circular(6), + onTap: () => _launchURL(file['url']), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFE6E6E6), + borderRadius: + BorderRadius.circular(6), + ), + child: const Icon( + Icons.download, + size: 18, + color: Colors.black, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ), + ], + ), + ) + ], + ), + ); + } + + Widget buildIRDocsContent({ + required bool isDesktop, + Key? key, + double? panelWidth, + }) { + return Container( + key: key ?? ValueKey("ir_docs_unified"), + width: isDesktop ? panelWidth ?? 320 : double.infinity, + height: double.infinity, + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + border: Border( + left: BorderSide( + color: Responsive.isDesktop(context) + ? Colors.grey.shade300 + : Colors.transparent, + width: Responsive.isDesktop(context) ? 1 : 0)), + ), + // color: Colors.white, + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ----------------------------- + // HEADER (same for mobile + web) + // ----------------------------- + Row( + children: [ + Expanded( + child: Text( + "Additional Documents", + style: GoogleFonts.poppins( + fontSize: isDesktop ? 16 : 18, + fontWeight: FontWeight.w600, + ), + ), + ), + IconButton( + icon: Icon(Icons.close), + tooltip: 'Remove', // Built-in property + onPressed: () { + _resetIRDocs(); + setState(() => showIRDocs = false); + }, + ), + ], + ), + + // SizedBox(height: isDesktop ? 8 : 8), + // + // Text( + // "Select documents, click Upload to assign the same file to all selected items.", + // style: GoogleFonts.poppins( + // fontSize: isDesktop ? 12 : 13, + // color: Colors.grey.shade700, + // ), + // ), + + SizedBox(height: 12), + + // ----------------------------- + // SCROLLABLE BODY + // ----------------------------- + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + // Checkbox list (same UI for both) + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, // ✅ two per row + crossAxisSpacing: 24, + mainAxisSpacing: 8, + childAspectRatio: + 5, // ✅ controls height (adjust if needed) + ), + itemCount: requiredDocsList.length, + itemBuilder: (context, index) { + final d = requiredDocsList[index]; + final title = d["document_name"] as String; + + return Row( + children: [ + Checkbox( + value: d["document_received"] as bool, + onChanged: (v) { + setState(() { + d["document_received"] = v; + }); + }, + ), + Expanded( + child: Text( + title, + style: const TextStyle(fontSize: 14), + ), + ), + ], + ); + }, + ), + // ...requiredDocsList.map((d) { + // final title = d["document_name"] as String; + // return CheckboxListTile( + // value: d["document_received"] as bool, + // onChanged: (v) => + // setState(() => d["document_received"] = v), + // title: Text(title), + // controlAffinity: isDesktop + // ? ListTileControlAffinity.leading + // : ListTileControlAffinity.trailing, + // contentPadding: EdgeInsets.symmetric(horizontal: 8), + // ); + // }).toList(), + + SizedBox(height: 12), + + // Multi uploader (same UI) + Padding( + padding: const EdgeInsets.all(16.0), + child: const MultiFileUploadWidget(forceMobile: true), + ), + + SizedBox(height: 12), + + // Assigned file preview + ..._assignedFiles.entries + .where((e) => e.value != null) + .map((e) { + final title = e.key; + final file = e.value!; + return Card( + margin: + EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: ListTile( + title: Text(title), + subtitle: Text( + file.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: IconButton( + icon: Icon(Icons.cancel, color: Colors.red), + tooltip: 'Remove', // Built-in property + onPressed: () => removeAssignedFile(title), + ), + ), + ); + }).toList(), + + SizedBox(height: 16), + ], + ), + ), + ), + + SizedBox(height: 8), + + // ----------------------------- + // SUBMIT BUTTON + // ----------------------------- + ElevatedButton( + onPressed: () => submitIRDocs(), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE26728), + minimumSize: Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ), + child: isSubmitting + ? SizedBox( + height: 22, + width: 22, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : Text( + 'Submit IR Docs', + style: GoogleFonts.poppins( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + SizedBox(height: 10), + + // ----------------------------- + // CANCEL BUTTON + // ----------------------------- + ElevatedButton( + onPressed: () { + _resetIRDocs(); + setState(() => showIRDocs = false); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + minimumSize: Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: const BorderSide(color: Color(0xFFE26728)), + ), + ), + child: Text( + "Cancel", + style: GoogleFonts.poppins( + color: Color(0xFFE26728), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ); + } + + // ------------------------- + // Mobile full IR Docs panel (replaces the main content) + // ------------------------- + Widget _buildMobileFullIrDocs(double panelWidth, {Key? key}) { + return Container( + key: key ?? ValueKey('mobile_full_ir'), + width: double.infinity, + height: double.infinity, + padding: EdgeInsets.all(12), + color: Colors.white, + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // header row with close (keeps popup header outside) + Row( + children: [ + Expanded( + child: Text("IR Docs", + style: GoogleFonts.poppins( + fontSize: 18, fontWeight: FontWeight.w600))), + IconButton( + icon: Icon(Icons.close), + tooltip: 'Close', // Built-in property + onPressed: () { + _resetIRDocs(); + setState(() => showIRDocs = false); + }, + ), + ], + ), + SizedBox(height: 8), + Text( + "Select documents, click Upload to assign the same file to all selected items.", + style: GoogleFonts.poppins( + fontSize: 13, color: Colors.grey.shade700)), + SizedBox(height: 12), + + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + ...requiredDocsList.map((d) { + final title = d['document_name'] as String; + return CheckboxListTile( + value: d['document_received'] as bool, + onChanged: (v) => + setState(() => d['document_received'] = v), + title: Text(title), + controlAffinity: ListTileControlAffinity.trailing, + contentPadding: EdgeInsets.symmetric(horizontal: 8.0), + ); + }).toList(), + + SizedBox(height: 12), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const MultiFileUploadWidget(forceMobile: true), + ], + ), + ), + // Padding( + // padding: const EdgeInsets.symmetric(horizontal: 8.0), + // child: ElevatedButton.icon( + // onPressed: handleUploadForSelected, + // icon: Icon(Icons.upload_file), + // label: Text("Upload for selected"), + // style: ElevatedButton.styleFrom( + // backgroundColor: Color(0xFF00A5A8), + // minimumSize: Size(double.infinity, 50), + // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + // ), + // ), + // ), + + SizedBox(height: 12), + + ..._assignedFiles.entries + .where((e) => e.value != null) + .map((e) { + final title = e.key; + final file = e.value!; + return Card( + margin: + EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: ListTile( + title: Text(title), + subtitle: Text(file.name, + maxLines: 1, overflow: TextOverflow.ellipsis), + trailing: IconButton( + icon: Icon(Icons.cancel, color: Colors.red), + tooltip: 'Remove', // Built-in property + onPressed: () => removeAssignedFile(title)), + ), + ); + }).toList(), + + SizedBox(height: 16), + ], + ), + ), + ), + + Container( + alignment: Alignment.center, + child: ElevatedButton( + onPressed: () { + submitIRDocs(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE26728), // Orange button + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Submit IR Docs', + style: GoogleFonts.poppins( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + SizedBox(height: 10), + Container( + alignment: Alignment.center, + child: ElevatedButton( + onPressed: () { + _resetIRDocs(); + setState(() => showIRDocs = false); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: const BorderSide(color: Color(0xFFE26728)), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Cancel', + style: + GoogleFonts.poppins(color: const Color(0xFFE26728)), + ), + ], + ), + ), + ) + ], + ), + ), + ); + } + + // ------------------------- + // Builds IR Docs panel (desktop) + // ------------------------- + Widget _buildIrDocsPanel(double panelWidth, {required bool isMobile}) { + // The panel has internal scrolling so it won't overflow + return Container( + width: panelWidth, + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + border: Border(left: BorderSide(color: Colors.grey.shade300, width: 1)), + ), + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // header row + Row( + children: [ + Expanded( + child: Text("IR Docs", + style: GoogleFonts.poppins( + fontSize: 16, fontWeight: FontWeight.w600))), + IconButton( + icon: Icon(Icons.close), + tooltip: 'Close', // Built-in property + onPressed: () { + _resetIRDocs(); + setState(() => showIRDocs = false); + }, + ) + ], + ), + SizedBox(height: 8), + Text( + "Select documents, click Upload to assign the same file to all selected items.", + style: GoogleFonts.poppins( + fontSize: 12, color: Colors.grey.shade700)), + SizedBox(height: 12), + + // list + upload + preview inside scroll + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + // checkboxes + ...requiredDocsList.map((d) { + final title = d['document_name'] as String; + return CheckboxListTile( + value: d['document_received'] as bool, + onChanged: (v) => + setState(() => d['document_received'] = v), + title: Text(title), + ); + }).toList(), + + SizedBox(height: 8), + + // upload button + // Padding( + // padding: const EdgeInsets.symmetric(horizontal: 4.0), + // child: ElevatedButton.icon( + // onPressed: handleUploadForSelected, + // icon: Icon(Icons.upload_file), + // label: Text("Upload for selected"), + // style: ElevatedButton.styleFrom( + // backgroundColor: Color(0xFF00A5A8), + // minimumSize: Size(double.infinity, 44), + // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)), + // ), + // ), + // ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const MultiFileUploadWidget(forceMobile: true), + ], + ), + ), + + SizedBox(height: 12), + + // assigned files preview + ..._assignedFiles.entries + .where((e) => e.value != null) + .map((e) { + final title = e.key; + final file = e.value!; + return Card( + child: ListTile( + title: Text(title), + subtitle: Text(file.name, + maxLines: 1, overflow: TextOverflow.ellipsis), + trailing: IconButton( + icon: Icon(Icons.cancel, color: Colors.red), + tooltip: 'Remove', // Built-in property + onPressed: () => removeAssignedFile(title)), + ), + ); + }).toList(), + + SizedBox(height: 16), + ], + ), + ), + ), + + // Submit button (keeps at bottom) + + Container( + alignment: Alignment.center, + child: ElevatedButton( + onPressed: () { + submitIRDocs(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE26728), // Orange button + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Submit IR Docs', + style: GoogleFonts.poppins( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + SizedBox(height: 10), + Container( + alignment: Alignment.center, + child: ElevatedButton( + onPressed: () { + _resetIRDocs(); + setState(() => showIRDocs = false); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: const BorderSide(color: Color(0xFFE26728)), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Cancel', + style: + GoogleFonts.poppins(color: const Color(0xFFE26728)), + ), + ], + ), + ), + ) + ], + ), + ), + ); + } + + // ------------------------- + // UI Helpers & existing functions below (unchanged) + // ------------------------- + Widget _buildStep({ + required int stepNumber, + required Widget title, + required Widget content, + bool isLast = false, + }) { + final noContent = (content as Column).children.isEmpty ? 0 : 1; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + SizedBox(height: stepNumber == 1 ? 0 : 4), + Container( + height: 28, + width: 28, + decoration: BoxDecoration( + color: Color(0xFF00A5A8), shape: BoxShape.circle), + alignment: Alignment.center, + child: Text( + '$stepNumber', + style: TextStyle( + color: Colors.white, + fontSize: Responsive.isDesktop(context) ? 13 : 12, + fontWeight: FontWeight.w600, + ), + ), + ), + if (!isLast) + Container( + height: noContent == 1 ? 50 : 20, + width: 2, + margin: EdgeInsets.only(top: 4, bottom: 4), + child: CustomPaint(painter: DottedLinePainter()), + ), + ], + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title, + if (noContent == 1) SizedBox(height: noContent == 0 ? 0 : 8), + if (noContent == 1) + Container( + width: double.infinity, + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Color(0xFFF7F7F7), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Color(0xFFE0E0E0)), + ), + child: content, + ), + if (noContent == 1) SizedBox(height: isLast ? 0 : 16), + ], + ), + ), + ], + ); + } + + Widget _getStepTitleFromApi(Map data) { + final status = (data['status'] ?? '').toString(); + final changedAt = + (data['changed_at'] ?? data['modified_at'] ?? '').toString(); + final modifiedBy = (data['modified_by'] ?? '').toString(); + final symbol = modifiedBy.isNotEmpty ? ' - ' : ''; + final isDesktop = Responsive.isDesktop(context); + + return RichText( + text: TextSpan( + children: [ + TextSpan( + text: status + (isDesktop ? ' ' : '\n'), + style: GoogleFonts.poppins( + fontSize: Responsive.isDesktop(context) ? 15 : 11, + fontWeight: FontWeight.w500, + color: Color(0xFF212120)), + ), + TextSpan( + text: changedAt.isEmpty ? '' : ' ($modifiedBy$symbol$changedAt)', + style: GoogleFonts.poppins( + fontSize: Responsive.isDesktop(context) ? 14 : 11, + fontWeight: FontWeight.w400, + color: Color(0xFF565656)), + ), + ], + ), + ); + } + + Widget _getStepContentFromApi(Map data) { + List rows = []; + data.forEach((key, value) { + if (key == 'modified_by' || + key == 'modified_at' || + key == 'status' || + key == 'changed_at') { + return; + } + if (value is! Map) return; + String displayName = value['display_name'] ?? key; + String displayValue = value['display_value'] ?? 'N/A'; + rows.add(_buildHistoryListData(displayName, displayValue)); + rows.add(SizedBox(height: 1)); + }); + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: rows); + } + + // Widget _buildKeyValue(String title, String value) { + // final displayValue = + // (value == null || value.trim().isEmpty) ? 'N/A' : value; + // return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Text(title, + // style: GoogleFonts.poppins( + // color: Color(0xFF747474), + // fontWeight: FontWeight.w400, + // fontSize: Responsive.isDesktop(context) ? 16 : 13)), + // SizedBox(height: 4), + // Text(displayValue, + // style: GoogleFonts.poppins( + // color: Color(0xFF000000), + // fontWeight: FontWeight.w500, + // fontSize: Responsive.isDesktop(context) ? 16 : 13)), + // ]); + // } + + Widget _buildKeyValue(String title, String? value) { + final displayValue = + (value == null || value.trim().isEmpty) ? 'N/A' : value.trim(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: GoogleFonts.poppins( + color: const Color(0xFF747474), + fontWeight: FontWeight.w400, + fontSize: Responsive.isDesktop(context) ? 16 : 13, + ), + ), + const SizedBox(height: 4), + + /// Prevent overflow everywhere (web & mobile) + Text( + displayValue, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: GoogleFonts.poppins( + color: const Color(0xFF000000), + fontWeight: FontWeight.w500, + fontSize: Responsive.isDesktop(context) ? 16 : 13, + ), + ), + ], + ); + } + + Widget _buildHistoryListData(String title, String value) { + final displayValue = + (value == null || value.trim().isEmpty) ? 'N/A' : value; + final titleText = Text(title, + style: GoogleFonts.poppins( + color: const Color(0xFF747474), + fontWeight: FontWeight.w400, + fontSize: Responsive.isDesktop(context) ? 14 : 12)); + final valueText = Text(displayValue, + style: GoogleFonts.poppins( + color: const Color(0xFF000000), + fontWeight: FontWeight.w400, + fontSize: Responsive.isDesktop(context) ? 14 : 12), + textAlign: TextAlign.right); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 1), + child: Responsive.isDesktop(context) + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, child: titleText)), + Expanded( + child: Align( + alignment: Alignment.centerRight, child: valueText)), + ]) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [titleText, SizedBox(height: 4), valueText]), + ); + } +} + +class DottedLinePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + const dashHeight = 2.0; + const dashSpace = 3.0; + double startY = 0; + final paint = Paint() + ..color = Colors.grey.shade400 + ..strokeWidth = 1; + while (startY < size.height) { + canvas.drawLine(Offset(0, startY), Offset(0, startY + dashHeight), paint); + startY += dashHeight + dashSpace; + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + diff --git a/lib/presentation/nonEBClaimsList.dart b/lib/presentation/nonEBClaimsList.dart new file mode 100644 index 0000000..21c7a90 --- /dev/null +++ b/lib/presentation/nonEBClaimsList.dart @@ -0,0 +1,1657 @@ +import 'dart:convert'; + +import 'package:csv/csv.dart'; +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:dropdown_search/dropdown_search.dart'; +import 'package:firebase_auth/firebase_auth.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 '../service/api_service.dart'; +import '../service/token_storage_service.dart'; +import 'RaiseClaimForm.dart'; +import 'package:nhancepolicy/logger.dart'; + +import 'nonEBClaimsCreate.dart'; +import 'nonEBClaimsHistory.dart'; + +class NonEBClaimsList extends StatefulWidget { + final String empCode; + const NonEBClaimsList({ + Key? key, + required this.empCode, + }); + + @override + State createState() => _NonEBClaimsListState(); +} + +class _NonEBClaimsListState extends State { + List tabHeader = [ + 'name', + 'empCode', + 'from', + 'to', + 'claimNum', + 'policyNumber' + ]; + final tokenService = TokenStorageService(); + List> filteredData = []; // Filtered data source + dynamic empClientId; + dynamic empClientBranchId; + dynamic empHrId; + String? _postPreToken = ''; + TextEditingController searchController = TextEditingController(); + TextEditingController searchClaimsStatusController = TextEditingController(); + + Map controllers = {}; + List reversedDataPolicy = []; + List> originalData = []; + String? selectedPolicyTypeName; + String? selectedClaimStatusName; + int? selectedPolicyType; + int? selectedClaimStatus; + + List> getClaimPolicies = []; + Map getClaimNonEBApi = {}; + bool isLoading = false; + late ApiService apiService; + int _currentPage = 1; + int _rowsPerPage = 5; + + List get _paginatedData { + final startIndex = (_currentPage - 1) * _rowsPerPage; + final endIndex = + (_currentPage * _rowsPerPage).clamp(0, filteredData.length); + return filteredData.sublist(startIndex, endIndex); + } + + int? appliedClaimStatus; + + Color getStatusColor(String status) { + switch (status.toLowerCase()) { + case 'claim received': + return const Color(0xFF4A90E2); // blue + case 'under process': + return const Color(0xFFF5A623); // orange + case 'information required': + return const Color(0xFFFF8C42); // amber + case 'approved': + return const Color(0xFF2ECC71); // green + case 'settled': + return const Color(0xFF1ABC9C); // teal-green + case 'rejected': + return const Color(0xFFE74C3C); // red + case 'denial review awaite': + return const Color(0xFF9B59B6); // purple + case 'closed': + return const Color(0xFFB0BEC5); // purple + default: + return const Color(0xFFB0BEC5); // grey fallback + } + } + + Map claim_Detials() { + final data = { + "page": 1, + "per_page": 200, + "client_id": empClientId ?? '', + "date_type": "created_date", + // "client_branch_id": empClientBranchId ?? '', + // "emp_code": controllers['empCode']?.text ?? '', + "start_date": controllers["from"]?.text ?? '', + "end_date": controllers["to"]?.text ?? '', + // "ticket_type_id": selectedPolicyType ?? '', + "claim_status_id": selectedClaimStatus ?? '', + "policy_no": controllers["policyNumber"]?.text ?? '', + // "acm_name": controllers["name"]?.text ?? '', + }; + return data; + } + + @override + void initState() { + super.initState(); + apiService = ApiService(context); + + for (String field in tabHeader) { + controllers[field] = TextEditingController(); + } + + // 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(); + _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.getNonEBStatusToApi(_postPreToken!, empClientId); + if (response['status'] == 'success' || response['status'] == true) { + setState(() { + isLoading = false; + }); + setState(() { + final data = response['data']; + if (data is List) { + // Non-EB API currently returns status master directly as a list. + getClaimNonEBApi = {'display_name': List.from(data)}; + } else if (data is Map) { + getClaimNonEBApi = Map.from(data); + } else { + getClaimNonEBApi = {}; + } + }); + } else { + setState(() { + isLoading = false; + }); + + // ToastHelper.showWarningToast( + // context, 'Request failed with status: ${response.statusCode}'); + logDebug('Request failed with status: ${response['code']}'); + } + } catch (e) { + setState(() { + // isLoading = false; + }); + logDebug('Exception occurred: $e'); + } finally { + setState(() { + // _isLoading = false; + }); + } + } + + Future getClaimList() async { + logDebug('getClaimList called'); + setState(() { + isLoading = true; + }); + + /// ✅ Resolve futures FIRST + empClientId ??= await tokenService.readValue('empClientId'); + empClientBranchId ??= await tokenService.readValue('empClientBranchId'); + + final requestData = claim_Detials(); + logDebug("Request Body: $requestData"); + + try { + final response = await apiService.getNonEBClaimsListDataToApi( + _postPreToken!, + requestData, + ); + + if (response['status'] == 'success' || response['status'] == true) { + setState(() { + getClaimPolicies = List>.from(response['data']); + logDebug('API Data - $getClaimPolicies'); + + originalData = getClaimPolicies; + filteredData = List.from(originalData); + logDebug('originalData - $getClaimPolicies'); + }); + } else { + getClaimPolicies = []; + originalData = getClaimPolicies; + filteredData = List.from(originalData); + logDebug('originalData - $getClaimPolicies'); + logDebug('Request failed: ${response['code']}'); + } + } catch (e) { + logDebug('Exception occurred: $e'); + } finally { + setState(() { + isLoading = false; + }); + } + } + + 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([ + 'Name', + 'Insured Name', + 'Policy Name', + 'Policy Number', + 'Claim Number', + 'Status', + ]); + + // Data rows + for (var item in data) { + rows.add([ + item['acm_name'] ?? '', + item['insured_contact_name'] ?? '', + item['policy_type_name'] ?? '', + item['policy_no'] ?? '', + item['claim_number'] ?? '', + item['status_display'] ?? '', + ]); + } + + // Convert to CSV string + String csvData = const ListToCsvConverter().convert(rows); + + // For Web: Create download + final bytes = utf8.encode(csvData); + final blob = html.Blob([bytes]); + final url = html.Url.createObjectUrlFromBlob(blob); + final anchor = html.AnchorElement(href: url) + ..setAttribute("download", "Claims.csv") + ..click(); + html.Url.revokeObjectUrl(url); + handleExportAction(); + } + + Future handleExportAction() async { + logDebug('handleExportAction'); + _postPreToken = await tokenService.getCurrentToken(); + final postId = await tokenService.readValue('empHrId'); + final preId = await tokenService.readValue('enrollmentEmpPrimaryId'); + var activity = "export_cddata"; + + logDebug('postId - $postId'); + logDebug('preId - $preId'); + logDebug('activity - $activity'); + + try { + logDebug('10'); + final response = await apiService.getPostLogHrActivity( + postId!, preId!, _postPreToken!, activity); + if (response['status'] == 'success') { + logDebug('Request success'); + } else { + // ToastHelper.showWarningToast( + // context, 'Request failed with status: ${response.statusCode}'); + logDebug('Request failed with status: ${response['code']}'); + } + } catch (e) { + logDebug('Exception occurred: $e'); + } + } + + void search(String query) { + logDebug(query); + // Check if the query is empty + if (query.isEmpty) { + // If search query is empty, show all data + setState(() { + filteredData = List.from(originalData); + }); + } else { + // Filter the original data based on the search query + setState(() { + filteredData = originalData.where((row) { + // Implement your filter logic here + // For example, check if any field in the row contains the query + // Adjust this logic based on your data structure + return row['acm_name'] + .toString() + .toLowerCase() + .contains(query.toLowerCase()) || + row['acm_name'] + .toString() + .toLowerCase() + .contains(query.toLowerCase()) || + row['insured_contact_name'] + .toString() + .toLowerCase() + .contains(query.toLowerCase()) || + row['policy_type_name'] + .toString() + .toLowerCase() + .contains(query.toLowerCase()) || + row['policy_no'] + .toString() + .toLowerCase() + .contains(query.toLowerCase()) || + row['claim_number'] + .toString() + .toLowerCase() + .contains(query.toLowerCase()) || + row['status_display'] + .toString() + .toLowerCase() + .contains(query.toLowerCase()); + }).toList(); + }); + } + logDebug(filteredData.length); + } + + Future _showLogoutDialog() async { + return await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: Text("Confirm Logout"), + content: Text("Do you want to logout?"), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text("Cancel"), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text("Logout"), + ), + ], + ), + ) ?? + false; + } + + // ✅ PUT IT HERE (inside State, outside build) + List> get claimStatusList { + final list = _claimStatusMaster; + + if (list is! List) return []; + + return list + .map>((e) => { + 'id': int.parse(e['id'].toString()), + 'name': e['display_name'].toString(), + }) + .toList(); + } + + List get _claimStatusMaster { + final directList = getClaimNonEBApi['display_name']; + if (directList is List) return directList; + + final fallbackData = getClaimNonEBApi['data']; + if (fallbackData is List) return fallbackData; + + return []; + } + + @override + Widget build(BuildContext context) { + return BaseLayout( + child: SecurePopScope( + child: _buildContent(context), + ), + ); + } + + Widget _buildContent(BuildContext context) { + return Container( + decoration: BoxDecoration( + // color: Colors.red.shade50, + // color: Colors.white, + borderRadius: BorderRadius.circular(10), // 👈 set your desired radius + ), + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + /// 🔙 Back + Title (LEFT) + Row( + children: [ + if (widget.empCode.isNotEmpty) + IconButton( + onPressed: () => { + Navigator.of(context).pop(), + }, + icon: const Icon( + Icons.arrow_back_ios, + size: 18, + color: Colors.black, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + const SizedBox(width: 6), + Text( + 'Claims', + style: GoogleFonts.poppins( + fontSize: 22, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + ], + ), + + /// Push right content to end + const Spacer(), + + /// 🔍 Search Box + Container( + width: 380, + height: 37, + decoration: BoxDecoration( + color: const Color(0xFFF0F0F0), + borderRadius: BorderRadius.circular(8), + ), + child: TextField( + controller: searchController, + onChanged: search, + style: GoogleFonts.poppins(fontSize: 14), + decoration: const InputDecoration( + hintText: 'Search', + prefixIcon: Icon(Icons.search, size: 18), + border: InputBorder.none, + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + ), + ), + + const SizedBox(width: 12), + + /// ⬇️ Export Button + SizedBox( + width: 116, + height: 37, + child: ElevatedButton( + onPressed: () { + exportToCsv(filteredData); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE26728), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: Text( + 'Export', + style: GoogleFonts.poppins( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Colors.white, + letterSpacing: 1, + ), + ), + ), + ), + const SizedBox(width: 12), + + /// ⬇️ Add Button + SizedBox( + width: 100, + height: 37, + child: ElevatedButton( + onPressed: () { + showDialog( + context: context, + barrierDismissible: true, + builder: (_) => NonEBClaimsCreate( + parentContext: context, // 👈 pass parent context + onSuccess: _loadIds, + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF00999E), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: Text( + 'Add', + style: GoogleFonts.poppins( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Colors.white, + letterSpacing: 1, + ), + ), + ), + ), + ], + ), + SizedBox(height: 20), + _buildInputFields(context), + const SizedBox(height: 10), + _buildStatusSummary(), + SizedBox( + height: 10, + ), + + isLoading + ? Expanded( + // color: Color(0x98FFFCE5), // Semi-transparent background + child: Center( + child: // Your GIF loader widget + Image.asset( + height: 60, + width: 60, + 'assets/nhance-loader.gif'), // Adjust path to your GIF loader + ), + ) + : Expanded( + child: _buildClaimsDataTable(context), // ✅ THIS IS REQUIRED + ), + // Expanded( + // child: Container( + // child: Column( + // children: [ + // // Text("DAer"), + // // _buildClaimsDataTable(context), + // SingleChildScrollView( + // scrollDirection: Axis.vertical, + // child: _buildClaimsDataTable(context), + // ), + // ], + // ), + // ), + // ), + ], + ), + ); + } + + Widget _buildInputFields(BuildContext context) { + return SizedBox( + height: 70, // 🔥 controls total height like image + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _buildFrom(context), + const SizedBox(width: 12), + _buildTo(context), + const SizedBox(width: 12), + _buildPolicyNumber(context), + const SizedBox(width: 12), + _buildClaimStatus(context), + const SizedBox(width: 12), + _buildActions(context), + ], + ), + ); + } + + Widget _buildClaimStatus(BuildContext context) { + return SizedBox( + width: MediaQuery.of(context).size.width * 0.15, + child: buildDropdownFieldSearch( + 'Claim Status', + (int? value) { + setState(() { + selectedClaimStatus = value; + }); + logDebug('Selected Claim Status ID: $selectedClaimStatus'); + }, + claimStatusList, + 'name', + selectedClaimStatus, + ), + ); + } + + Widget _buildClaimsDataTable(BuildContext context) { + if (filteredData.isEmpty) { + return const Center(child: Text('No available data')); + } + + return CustomScrollView( + slivers: [ + /// 🔒 FIXED HEADER + SliverPersistentHeader( + pinned: true, + delegate: _ClaimsHeaderDelegate(), + ), + + /// 📄 TABLE ROWS + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final item = _paginatedData[index]; + return _buildDataRow(item); + }, + childCount: _paginatedData.length, + ), + ), + + /// 📌 PAGINATION + SliverToBoxAdapter( + child: _buildPagination(context), + ), + ], + ); + } + + Widget _buildDataRow(Map item) { + 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(item['status_display'] ?? ''), + borderRadius: BorderRadius.circular(10), + ), + child: Text(item['status_display'] ?? '-', 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, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildStatusSummary() { + final List statusMaster = _claimStatusMaster; + + if (statusMaster.isEmpty) return const SizedBox(); + + final List displayStatuses = appliedClaimStatus != null + ? statusMaster + .where((status) => + int.parse(status['id'].toString()) == appliedClaimStatus) + .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]['display_name']?.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 _buildFrom(BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width * 0.15, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "From", + style: TextStyle(fontSize: 12), + ), + const SizedBox( + height: 3, + ), + SizedBox( + height: 40, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), // soft shadow + blurRadius: 8, + offset: const Offset(0, 2), // downward shadow + ), + ], + ), + child: TextField( + controller: controllers['from'], + readOnly: true, + onTap: () async { + FocusScope.of(context) + .requestFocus(FocusNode()); // hide keyboard + DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime.now(), + ); + + if (pickedDate != null) { + String formattedDate = + "${pickedDate.day.toString().padLeft(2, '0')}-${pickedDate.month.toString().padLeft(2, '0')}-${pickedDate.year}"; + + logDebug("FomatedFromDAta - $formattedDate"); + setState(() { + controllers['from']?.text = formattedDate; + controllers['to'] + ?.clear(); // 🔥 prevent invalid To date + }); + } + }, + style: const TextStyle( + fontSize: 12, + ), + decoration: InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.never, + + hintText: 'Select', + filled: true, + fillColor: Colors.white, + + border: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.circular(8.0), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.circular(8.0), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.teal, width: 2.0), + borderRadius: BorderRadius.circular(8.0), + ), + // contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), + contentPadding: EdgeInsets.fromLTRB(12, 20, 12, 10), + suffixIcon: const Icon(Icons.calendar_today, size: 16), + ), + ), + )), + ], + ), + ); + } + + Widget _buildTo(BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width * 0.15, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "To", + style: TextStyle(fontSize: 12), + ), + const SizedBox( + height: 3, + ), + SizedBox( + height: 40, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), // soft shadow + blurRadius: 8, + offset: const Offset(0, 2), // downward shadow + ), + ], + ), + child: TextField( + controller: controllers['to'], + readOnly: true, + onTap: () async { + FocusScope.of(context).requestFocus(FocusNode()); + + final fromDate = _parseDate(controllers['from']?.text); + + DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: fromDate ?? DateTime.now(), + firstDate: fromDate ?? + DateTime(2000), // ✅ cannot be before From date + lastDate: DateTime.now(), // ✅ no future dates + ); + + if (pickedDate != null) { + String formattedDate = + "${pickedDate.day.toString().padLeft(2, '0')}-" + "${pickedDate.month.toString().padLeft(2, '0')}-" + "${pickedDate.year}"; + + setState(() { + controllers['to']?.text = formattedDate; + }); + } + }, + style: const TextStyle( + fontSize: 12, + ), + decoration: InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.never, + + hintText: 'Select', + + filled: true, + fillColor: Colors.white, + + border: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.circular(8.0), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.circular(8.0), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.teal, width: 2.0), + borderRadius: BorderRadius.circular(8.0), + ), + // contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), + contentPadding: EdgeInsets.fromLTRB(12, 20, 12, 10), + suffixIcon: const Icon(Icons.calendar_today, size: 16), + ), + ), + )), + ], + ), + ); + } + + Widget buildDropdownFieldSearch( + String label, + void Function(int?) onChanged, + List> itemsList, + String displayField, + int? selectedValue, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 4), + Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: DropdownButtonHideUnderline( + child: DropdownButton2( + isExpanded: true, + value: selectedValue, + hint: const Text( + 'Select', + style: TextStyle(fontSize: 12), + ), + + iconStyleData: const IconStyleData( + icon: Icon(Icons.keyboard_arrow_down), + ), + + dropdownStyleData: DropdownStyleData( + maxHeight: 260, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + ), + + // 🔍 SEARCH SUPPORT + dropdownSearchData: DropdownSearchData( + searchController: searchClaimsStatusController, + searchInnerWidgetHeight: 50, + searchInnerWidget: Padding( + padding: const EdgeInsets.all(8), + child: TextField( + controller: searchClaimsStatusController, + style: const TextStyle(fontSize: 12), + decoration: InputDecoration( + hintText: 'Search...', + hintStyle: const TextStyle(fontSize: 12), + isDense: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + searchMatchFn: (item, searchValue) { + final matchedItem = itemsList.firstWhere( + (e) => e['id'] == item.value, + orElse: () => {}, + ); + + final text = + matchedItem['name']?.toString().toLowerCase() ?? ''; + return text.contains(searchValue.toLowerCase()); + }, + ), + + onMenuStateChange: (isOpen) { + if (!isOpen) { + searchClaimsStatusController.clear(); + } + }, + + items: itemsList.map((item) { + return DropdownMenuItem( + value: item['id'], + child: Text( + item[displayField] ?? '', + style: const TextStyle(fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ); + }).toList(), + + onChanged: onChanged, + ), + ), + ), + ], + ); + } + + DateTime? _parseDate(String? value) { + if (value == null || value.isEmpty) return null; + final parts = value.split('-'); + return DateTime( + int.parse(parts[2]), + int.parse(parts[1]), + int.parse(parts[0]), + ); + } + + Widget _buildPolicyNumber(BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width * 0.15, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Policy Number", + style: TextStyle(fontSize: 12), + ), + const SizedBox( + height: 3, + ), + SizedBox( + height: 40, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), // soft shadow + blurRadius: 8, + offset: const Offset(0, 2), // downward shadow + ), + ], + ), + child: TextField( + controller: controllers['policyNumber'], + style: const TextStyle( + fontSize: 12, + ), + decoration: InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.never, + + hintText: 'Policy Number', + + filled: true, + fillColor: Colors.white, + + border: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.circular(8.0), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.circular(8.0), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.teal, width: 2.0), + borderRadius: BorderRadius.circular(8.0), + ), + // contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), + contentPadding: EdgeInsets.fromLTRB(12, 20, 12, 10), + ), + ), + )), + ], + ), + ); + } + + Widget _buildActions(BuildContext context) { + return SizedBox( + width: 100, // enough for 2 icons + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + /// 🔹 Empty label space (matches "From / To / Policy Number") + const SizedBox( + height: 20, // same height as label text + spacing + ), + + /// 🔹 Icon row aligned with input fields + SizedBox( + height: 40, // EXACT same height as input fields + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _IconActionButton( + icon: Icons.filter_alt_outlined, + onTap: applyFilter, + tooltip: "Filter"), + const SizedBox(width: 12), + _IconActionButton( + icon: Icons.refresh_outlined, + onTap: reset, + tooltip: "Reset"), + ], + ), + ), + ], + ), + ); + } +} + +class _ClaimsHeaderDelegate extends SliverPersistentHeaderDelegate { + @override + double get minExtent => 52; + + @override + double get maxExtent => 52; + + @override + Widget build( + BuildContext context, double shrinkOffset, bool overlapsContent) { + return Container( + // color: const Color(0xFF00A6A6), + decoration: BoxDecoration( + color: Color(0xFF00A6A6), + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + alignment: Alignment.centerLeft, + child: const Row( + children: [ + Expanded(flex: 4, child: Text('Name', style: _headerStyle)), + Expanded(flex: 3, child: Text('Insured Name', style: _headerStyle)), + Expanded(flex: 4, child: Text('Policy Name', style: _headerStyle)), + Expanded(flex: 3, child: Text('Claim Number', style: _headerStyle)), + Expanded(flex: 4, child: Text('Status', style: _headerStyle)), + Expanded( + flex: 2, + child: Text('Action', + textAlign: TextAlign.center, style: _headerStyle), + ), + ], + ), + ); + } + + static const _headerStyle = TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ); + + @override + bool shouldRebuild(_) => false; +} + +// ================= 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), + ), + ), + ), + ); + } +} + diff --git a/lib/service/api_service.dart b/lib/service/api_service.dart index 8434cf9..648d7f8 100755 --- a/lib/service/api_service.dart +++ b/lib/service/api_service.dart @@ -719,6 +719,20 @@ class ApiService { return response; } + + Future> getNonEBStatusToApi( + String token, empClientId) async { + logDebug("getgetClaimPoliciesToApii1"); + final url = Uri.parse( + '${Environment.apiUrlPost}api/v1/non-eb-claim/statuses'); + + final headers = { + 'Authorization': 'Bearer $token' ?? '', + }; + final response = await _makeGetRequest(url, headers); + return response; + } + Future> getClaimPoliciesFileDownload( id, String token) async { final url = Uri.parse('${Environment.apiUrlPost}hrFileDownload?id=$id'); @@ -755,6 +769,31 @@ class ApiService { } } + Future> getNonEBClaimsListDataToApi( + String token, Map body) async { + logDebug("getgetClaimPoliciesToApii1"); + final url = Uri.parse('${Environment.apiUrlPost}api/v1/non-eb-claim/list'); + + final headers = { + 'APP-SIGNATURE': + 'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y', + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }; + + final response = await http.post( + url, + headers: headers, + body: jsonEncode(body), + ); + if (response.statusCode == 200) { + return jsonDecode(response.body); + } else { + throw Exception( + 'Failed to load claims list: ${response.statusCode} ${response.body}'); + } + } + Future> getEcardRequest(eCardParam, String token) async { final url = Uri.parse('${Environment.apiUrlPost}ecardRequest'); final headers = { @@ -768,6 +807,20 @@ class ApiService { return response; } + Future> getNonEBClaimPoliciesToApi( + String token, request) async { + final url = Uri.parse('${Environment.apiUrlPost}api/v1/non-eb-claim/policies'); + final headers = { + 'Authorization': 'Bearer $token' ?? '', + 'Content-Type': 'application/json', // Ensure content type is JSON + }; + final jsonBody = jsonEncode(request); + // Send formDataJson as the body + final response = + await _makePostRequestWithoutFormData(url, jsonBody, headers); + return response; + } + Future> getCdTransactionData( String clintID, String insurerId, String cd_ac_pk, String token) async { logDebug("getCashDepositDetailsToApi1"); @@ -819,6 +872,19 @@ class ApiService { return response; } + Future> getNonEBClaimsHistoryToApi( + String ticket_type_id, String token) async { + logDebug("getCashDepositDetailsToApi1"); + final url = Uri.parse( + '${Environment.apiUrlPost}api/v1/non-eb-claim/history/${ticket_type_id}'); + + final headers = { + 'Authorization': 'Bearer $token' ?? '', + }; + final response = await _makeGetRequest(url, headers); + return response; + } + Future> getSampleFileDownload(String token) async { final url = Uri.parse('${Environment.apiUrl}downloadSampleExcel/enrollment'); diff --git a/lib/service/nonEBClaimsService.dart b/lib/service/nonEBClaimsService.dart new file mode 100644 index 0000000..bf750d7 --- /dev/null +++ b/lib/service/nonEBClaimsService.dart @@ -0,0 +1,107 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:file_picker/file_picker.dart'; +import 'package:http/http.dart' as http; + +import '../config/environment.dart'; + +class NonEBClaimsService { + static const String _appSignature = + 'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y'; + + Map _headers(String token, {bool json = true}) { + return { + 'Authorization': 'Bearer $token', + 'APP-SIGNATURE': _appSignature, + if (json) 'Content-Type': 'application/json', + }; + } + + Future> createClaim({ + required String token, + required Map fields, + required PlatformFile assetFile, + }) async { + final request = http.MultipartRequest( + 'POST', + Uri.parse('${Environment.apiUrlPost}non-eb-claim/create'), + ); + + request.headers.addAll(_headers(token, json: false)); + request.fields.addAll(fields); + + final Uint8List? bytes = assetFile.bytes; + if (bytes == null) { + throw Exception('Selected file has no bytes'); + } + + request.files.add( + http.MultipartFile.fromBytes( + 'asset_file', + bytes, + filename: assetFile.name, + ), + ); + + final streamed = await request.send(); + final body = await streamed.stream.bytesToString(); + return jsonDecode(body) as Map; + } + + Future> listClaims({ + required String token, + required Map body, + }) async { + final response = await http.post( + Uri.parse('${Environment.apiUrlPost}v1/non-eb-claim/list'), + headers: _headers(token), + body: jsonEncode(body), + ); + return jsonDecode(response.body) as Map; + } + + Future> getHistory({ + required String token, + required String claimId, + }) async { + final response = await http.get( + Uri.parse('${Environment.apiUrlPost}non-eb-claim/history/$claimId'), + headers: _headers(token, json: false), + ); + return jsonDecode(response.body) as Map; + } + + Future> uploadRequiredDoc({ + required String token, + required String claimId, + required String documentName, + required PlatformFile file, + }) async { + final request = http.MultipartRequest( + 'POST', + Uri.parse( + '${Environment.apiUrlPost}non-eb-claim/$claimId/upload-required-doc'), + ); + + request.headers.addAll(_headers(token, json: false)); + request.fields['document_name'] = documentName; + + final Uint8List? bytes = file.bytes; + if (bytes == null) { + throw Exception('Selected file has no bytes'); + } + + request.files.add( + http.MultipartFile.fromBytes( + 'file', + bytes, + filename: file.name, + ), + ); + + final streamed = await request.send(); + final body = await streamed.stream.bytesToString(); + return jsonDecode(body) as Map; + } +}