This commit is contained in:
Surendiran 2026-04-16 17:57:22 +05:30
parent f940e53501
commit 3b323b4ce2
10 changed files with 4460 additions and 37 deletions

View File

@ -17,6 +17,10 @@ class NhanceSideBar extends StatefulWidget {
class _NhanceSideBarState extends State<NhanceSideBar> {
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<NhanceSideBar> {
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<NhanceSideBar> {
// ),
...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<NhanceSideBar> {
),
),
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,
),
),
),
),
),
);

View File

@ -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<String, WidgetBuilder> appRoutes = {
'ClaimsPolicies': (context) => ClaimsPolicies(
empCode: '',
),
'nonEBClaimsList': (context) => const NonEBClaimsList(empCode: '',),
'cdTransactionDetails': (context) => cdTransactionDetails(
insurerName: '',
cdMasterAccountNo: '',

View File

@ -156,7 +156,7 @@ class _ClaimsPolicieState extends State<ClaimsPolicies> {
_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<ClaimsPolicies> {
try {
logDebug('10');
final response =
await apiService.getClaimPoliciesToApi(_postPreToken!, '');
await apiService.getClaimPoliciesToApi(_postPreToken!, empClientId);
if (response['status'] == 'success') {
setState(() {
isLoading = false;

View File

@ -661,13 +661,11 @@ class _ClaimHistoryPopupState extends State<ClaimHistoryPopup>
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(stepKeys.length, (index) {
String stepTitleKey = stepKeys[index];
Map<String, dynamic> stepData =
stepMap[stepTitleKey];
Map<String, dynamic> 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,
);

View File

@ -131,9 +131,13 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
List<String> currentPageIds = [];
List<dynamic> 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<hrPolicyDetails>
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<hrPolicyDetails>
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']

View File

@ -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<NonEBClaimsCreate> createState() => _NonEBClaimsCreateState();
}
class _NonEBClaimsCreateState extends State<NonEBClaimsCreate> {
bool isLoading = false;
dynamic empClientId;
dynamic empClientBranchId;
final tokenService = TokenStorageService();
String? _postPreToken = '';
List<Map<String, dynamic>> policyNumberList = [];
int? policyNumberId;
bool isSubmitting = false;
Map<String, dynamic> 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<void> _loadToken() async {
_postPreToken = tokenService.getCurrentToken();
empClientId = await tokenService.readValue('empClientId');
empClientBranchId = await tokenService.readValue('empClientBranchId');
getClaimsPoliciesDetails();
}
Future<void> 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 : <dynamic>[];
policyNumberList = rawList.map<Map<String, dynamic>>((policy) {
final map = Map<String, dynamic>.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<void> 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 = <String, dynamic>{
'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<void> 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<Widget> 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<TextInputFormatter>? 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<Map<String, dynamic>> 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<int>(
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<Widget>((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<DropdownMenuItem<int>>((item) {
return DropdownMenuItem<int>(
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<DateTime?> 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,
);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -719,6 +719,20 @@ class ApiService {
return response;
}
Future<Map<String, dynamic>> 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<Map<String, dynamic>> getClaimPoliciesFileDownload(
id, String token) async {
final url = Uri.parse('${Environment.apiUrlPost}hrFileDownload?id=$id');
@ -755,6 +769,31 @@ class ApiService {
}
}
Future<Map<String, dynamic>> getNonEBClaimsListDataToApi(
String token, Map<String, dynamic> 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<Map<String, dynamic>> getEcardRequest(eCardParam, String token) async {
final url = Uri.parse('${Environment.apiUrlPost}ecardRequest');
final headers = {
@ -768,6 +807,20 @@ class ApiService {
return response;
}
Future<Map<String, dynamic>> 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<Map<String, dynamic>> getCdTransactionData(
String clintID, String insurerId, String cd_ac_pk, String token) async {
logDebug("getCashDepositDetailsToApi1");
@ -819,6 +872,19 @@ class ApiService {
return response;
}
Future<Map<String, dynamic>> 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<Map<String, dynamic>> getSampleFileDownload(String token) async {
final url =
Uri.parse('${Environment.apiUrl}downloadSampleExcel/enrollment');

View File

@ -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<String, String> _headers(String token, {bool json = true}) {
return {
'Authorization': 'Bearer $token',
'APP-SIGNATURE': _appSignature,
if (json) 'Content-Type': 'application/json',
};
}
Future<Map<String, dynamic>> createClaim({
required String token,
required Map<String, String> 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<String, dynamic>;
}
Future<Map<String, dynamic>> listClaims({
required String token,
required Map<String, dynamic> 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<String, dynamic>;
}
Future<Map<String, dynamic>> 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<String, dynamic>;
}
Future<Map<String, dynamic>> 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<String, dynamic>;
}
}