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, );