import 'package:dropdown_search/dropdown_search.dart'; 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:http/http.dart' as ref; import 'package:toastification/toastification.dart'; import '../../../../core/config/env.dart'; import '../../../../core/services/api_service.dart'; import '../../../../data/services/auth_service.dart'; import '../../../../data/utils/toastNotification.dart'; import '../../../../data/utils/validators.dart'; import '../../../layouts/responsive_layout.dart'; import '../../../themes/indicators/customizd_file_upload.dart'; import '../../../themes/indicators/input_field_decoration.dart'; import '../../../themes/indicators/search_field_theme.dart'; import '../../../themes/indicators/text_field_theme.dart'; import '../../../providers/manager_provider.dart'; final decimalFormatter = [ FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')), ]; class UpdateEndorsementDialog extends StatefulWidget { final Map item; final dynamic userId; final dynamic managerId; final Function(String) onSubmit; const UpdateEndorsementDialog({ super.key, required this.item, required this.userId, required this.managerId, required this.onSubmit, }); @override State createState() => _UpdateEndorsementDialogState(); } class _UpdateEndorsementDialogState extends State { late ApiService apiService; String? _token; final _formKeyUpdate = GlobalKey(); Map controllers = {}; List> getEndrosmentType = []; List> filteredEndrosmentData = []; String? selectedEndorsement; String? selectedPolicyFrom; String? selectedStatus; // "Pending" or "Completed" String? selectedFinancialStatus; String? selectedFileNames; bool isLoading = false; PlatformFile? docUploadedFile; final List> statusOptions = [ {"status_value": "Open"}, {"status_value": "Pending"}, {"status_value": "Completed"}, ]; final List> policyFromOptions = [ {"policy_from": "Internal"}, {"policy_from": "External"}, ]; final List> financialOptions = [ { "financial_value": "Yes"}, { "financial_value": "No"}, ]; final GlobalKey>> dropDownKey = GlobalKey>>(); final GlobalKey>> dropDownKeyEndorsement = GlobalKey>>(); @override void initState() { apiService = ApiService(); super.initState(); _initializeToken(); print("secondpage"); // 1. Initialize Controllers controllers['endorsement_no'] = TextEditingController(text: widget.item['endorsement_no']); controllers['remarks'] = TextEditingController(text: widget.item['endorsement_description']); controllers['contact_person'] = TextEditingController(text: widget.item['contact_person']); controllers['endorsement_premium'] = TextEditingController(text: widget.item['endorsement_premium']); controllers['pending_days'] = TextEditingController(text: widget.item['pending_days']); selectedEndorsement = widget.item['endorsement_type']?.toString(); selectedStatus = widget.item['status']?.toString(); selectedFinancialStatus = widget.item['financia_or_non_financial']?.toString(); selectedFileNames = widget.item['endorsement_completion_file']; getEnroementType(); } Future _initializeToken() async { _token = await AuthService.getToken(); print("APISERTOKEN - $_token"); } // API Call logic // Future updateData() async { // if (!_formKeyUpdate.currentState!.validate()) return; // // // VALIDATION: If Status is Completed, File is Mandatory // if (selectedStatus == "Completed" && docUploadedFile == null) { // ToastHelper.showErrorToast(context,"Document is required for Completed status",); // return; // } // // // setState(() => isLoading = true); // // try { // final Uri uri = Uri.parse('${Env.apiUrl}endorsement/updateEndorsement'); // // if (_token == null) { // throw Exception('Token not found. Please log in.'); // } // // // Use MultipartRequest (POST only) // final request = http.MultipartRequest('POST', uri); // request.headers['Authorization'] = 'Bearer $_token'; // request.headers['app-signature'] = Env.App_Signature; // // // Mapping fields to API Table // request.fields['endorsement_type'] = selectedEndorsement ?? ""; // request.fields['endorsement_description'] = controllers['remarks']?.text ?? ""; // request.fields['endorsement_no'] = widget.item['endorsement_no'] ?? ""; // // request.fields['updated_by'] = userId; // request.fields['status'] = selectedStatus ?? ""; // // if (docUploadedFile != null) { // try { // if (docUploadedFile!.bytes != null) { // final multipartFile = http.MultipartFile.fromBytes( // 'endorsement_completion_file', // docUploadedFile!.bytes!, // filename: docUploadedFile!.name, // ); // request.files.add(multipartFile); // } // // print("📎 File attached: ${docUploadedFile!.name}"); // } catch (e) { // print("❌ Failed to attach file: $e"); // } // } // // final response = await request.send(); // if (response.statusCode == 200) { // ToastHelper.showSuccessToast(context, 'Updated Successfully'); // Navigator.pop(context); // widget.onSubmit("success"); // } // } catch (e) { // print("Error: $e"); // } finally { // setState(() => isLoading = false); // } // } Future updateData() async { // 1. Basic Form Validation if (!_formKeyUpdate.currentState!.validate()) return; // 2. Business Validation: File mandatory for 'Completed' status if (selectedStatus == "Completed" && docUploadedFile == null) { ToastHelper.showErrorToast(context, "Document is required for Completed status"); return; } setState(() => isLoading = true); try { final Uri uri = Uri.parse('${Env.apiUrl}endorsement/updateEndorsement'); // final Uri uri = Uri.parse('http://localhost/nhance_partner_be/endorsement/updateEndorsement'); if (_token == null) throw Exception('Token not found. Please log in.'); // Create Multipart Request final request = http.MultipartRequest('POST', uri); request.headers['Authorization'] = 'Bearer $_token'; request.headers['app-signature'] = Env.App_Signature; // --- MAPPING FIELDS --- // Use widget.item['id'] to identify the unique record request.fields['id'] = widget.item['id'].toString(); request.fields['endorsement_type'] = selectedEndorsement ?? ""; request.fields['endorsement_description'] = controllers['remarks']?.text ?? ""; request.fields['endorsement_no'] = controllers['endorsement_no']?.text ?? ""; request.fields['contact_person'] = controllers['contact_person']?.text ?? ""; request.fields['endorsement_premium'] = controllers['endorsement_premium']?.text ?? ""; request.fields['pending_days'] = controllers['pending_days']?.text ?? ""; // Set updated_by from the widget parameter request.fields['updated_by'] = widget.userId.toString(); request.fields['status'] = selectedStatus ?? ""; request.fields['financia_or_non_financial'] = selectedFinancialStatus ?? ""; // --- FILE UPLOAD LOGIC --- if (docUploadedFile != null) { if (docUploadedFile!.bytes != null) { // For Web/Bytes request.files.add(http.MultipartFile.fromBytes( 'endorsement_completion_file', // Key must match Backend docUploadedFile!.bytes!, filename: docUploadedFile!.name, )); } else if (docUploadedFile!.path != null) { // For Mobile/Desktop path request.files.add(await http.MultipartFile.fromPath( 'endorsement_completion_file', docUploadedFile!.path!, filename: docUploadedFile!.name, )); } print("📎 Attached: ${docUploadedFile!.name}"); } // 3. Send Request final response = await request.send(); final responseData = await http.Response.fromStream(response); if (response.statusCode == 200) { ToastHelper.showSuccessToast(context, 'Updated Successfully'); Navigator.pop(context); widget.onSubmit("success"); } else { print("Server Error: ${responseData.body}"); ToastHelper.showErrorToast(context, "Update failed: ${response.statusCode}"); } } catch (e) { print("Error during update: $e"); ToastHelper.showErrorToast(context, "An error occurred"); } finally { setState(() => isLoading = false); } } @override Widget build(BuildContext context) { return SelectionArea( child: AlertDialog( backgroundColor: Colors.white, // Add shape for a cleaner look shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), content: SizedBox( // Set a width to prevent layout jumping width: MediaQuery.of(context).size.width * 0.6, child: SingleChildScrollView( // 👈 Fix 1: Prevent Overflow child: Column( mainAxisSize: MainAxisSize.min, children: [ // Header row Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Update Endorsement", style: GoogleFonts.inter( color: const Color(0xFF374141), fontSize: 18, fontWeight: FontWeight.bold, ), ), GestureDetector( onTap: () => Navigator.pop(context), child: Container( padding: const EdgeInsets.all(5.0), decoration: BoxDecoration( color: const Color(0xFFF1F1F1), borderRadius: BorderRadius.circular(5.0), ), child: const Tooltip( message: 'Close', child: Icon(Icons.close, size: 18), ), ), ), ], ), const SizedBox(height: 16), // Your form content endorsement(context), ], ), ), ), actions: [ GestureDetector( onTap: () { // 👈 Fix 2: Remove "onPressed:" label. Just call the function. updateData(); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 25.0, vertical: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.0), color: const Color(0xFF425B5B), ), child: isLoading ? const SizedBox(height: 15, width: 15, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : const Text('Save', style: TextStyle(color: Colors.white)), ), ), ], ) ); } Widget endorsement(BuildContext context) { return Form( key: _formKeyUpdate, child: Column( children: [ Row( children: [ // Wrap children in Expanded so they know how much space to take in the Row Expanded(child: buildEndrosType(context)), const SizedBox(width: 15), Expanded(child: buildStatusDropdown(context)), ], ), const SizedBox(height: 10), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: buildPolicyNumber(context)), const SizedBox(width: 15), Expanded(child: buildUploadDocumentEndorsment(context)), ], ), const SizedBox(height: 10), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: buildContactPerson(context)), const SizedBox(width: 15), Expanded(child: buildFinancialOrNonFinancial(context)), ], ), const SizedBox(height: 10), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: buildEndPremium(context)), const SizedBox(width: 15), Expanded(child: buildPendingDays(context)), ], ), const SizedBox(height: 10), Row( children: [ Expanded(child: buildEndrosRemarks(context)), const SizedBox(width: 15), const Spacer(), // Keeps the remark field to the left ], ), ], ), ); } Future getEnroementType() async { print('Insurers called'); setState(() { isLoading = true; }); try { final response = await apiService.fetchMasterDropDown('Endorsement'); if (response['status'] == 200) { print('getEnroementType - ${response['data']}'); setState(() { getEndrosmentType = List>.from(response['data']); print('API Data - $getEndrosmentType'); filteredEndrosmentData = List.from(getEndrosmentType); print('originalData - $filteredEndrosmentData'); }); } else { getEndrosmentType = []; filteredEndrosmentData = []; } } catch (e) { print('Exception occurred: $e'); } finally { setState(() { isLoading = false; }); } } // --- Specialized Widgets --- Widget buildEndrosType(context) { Map? selectedEndorsementd = filteredEndrosmentData .firstWhere( (item) => item['id'].toString() == selectedEndorsement, orElse: () => {}, ); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Endorsement Type *", style: _textStyle), SizedBox(height: 10), Container( color: Colors.white, width: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.26, // height: 40, child: DropdownSearch>( key: dropDownKeyEndorsement, selectedItem: selectedEndorsementd.isNotEmpty ? selectedEndorsementd : null, items: (filter, infiniteScrollProps) { return filteredEndrosmentData; }, itemAsString: (val) => val['endorsement_type'].toString(), // what to show compareFn: (item, selectedItem) => item['id'] == selectedItem['id'], // ✅ compare by id validator: (val) { if (val == null) { return "Required"; // ✅ error message } return null; }, decoratorProps: DropDownDecoratorProps( decoration: AppInputDecorations.dropdownDecoration( label: "Select Endorsement Type", ).copyWith( filled: true, fillColor: Color( 0xFFEDF6F5,), // 👈 makes the dropdown input white isDense: true, // 👈 Makes the field compact contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 0), // 👈 Vertical 0 helps center the text ), ), popupProps: PopupProps.menu( fit: FlexFit.loose, constraints: BoxConstraints(maxHeight: 250), menuProps: MenuProps( backgroundColor: Colors.white, // 👈 sets dropdown background to white ), showSearchBox: true, searchFieldProps: TextFieldProps( decoration: InputDecoration( filled: true, fillColor: Colors.white, hintText: "Search Endorsement Type...", enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.white, ), // 👈 Normal border ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.white, width: 1.5, ), // 👈 Focused border ), ), ), // constraints: BoxConstraints(), ), onChanged: (val) { if (val != null) { print("Selected Endorsement : ${val['endorsement_type']}"); print("Id: ${val['id']}"); selectedEndorsement = val['id']; // controllers['agentId']?.text = val['agent_code']; // agentId = agent['id']; } }, ), ), ], ); } Widget buildPolicyNumber(context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Endorsement Number *", style: _textStyle), SizedBox(height: 10), ThemedFormField( backgroundColor: Color(0xFFEDF6F5), controller: controllers['endorsement_no']!, validator: (value) => Validators.requiredField(value, "endorsement_no"), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9\-_]')), ], txtwidth: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.26, // keyboardType: TextInputType.multiline, // txtheight: 45, ), ], ); } Widget buildStatusDropdown(context) { // Find the selected item from the static list based on your variable Map selectedEndorsementST = statusOptions.firstWhere( (item) => item['status_value'].toString() == selectedStatus, orElse: () => {}, ); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Status *", style: _textStyle), const SizedBox(height: 10), Container( color: Colors.white, width: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.26, child: DropdownSearch>( // Use a unique key for this dropdown key: const ValueKey("statusDropdown"), selectedItem: selectedEndorsementST.isNotEmpty ? selectedEndorsementST : null, items: (filter, infiniteScrollProps) { return statusOptions; // Use the static list }, itemAsString: (val) => val['status_value'].toString(), compareFn: (item, selectedItem) => item['status_value'] == selectedItem['status_value'], // --- REQUIRED VALIDATOR --- validator: (val) { if (val == null || val.isEmpty) { return "Required"; } return null; }, decoratorProps: DropDownDecoratorProps( decoration: AppInputDecorations.dropdownDecoration( label: "Select Status", ).copyWith( filled: true, fillColor: Color( 0xFFEDF6F5,), // 👈 makes the dropdown input white isDense: true, // 👈 Makes the field compact contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 0), // 👈 Vertical 0 helps center the text ), ), popupProps: PopupProps.menu( fit: FlexFit.loose, constraints: const BoxConstraints(maxHeight: 150), // Shorter for 2 items menuProps: const MenuProps( backgroundColor: Colors.white, ), showSearchBox: true, // Disabled search as there are only 2 items searchFieldProps: TextFieldProps( decoration: InputDecoration( filled: true, fillColor: Colors.white, hintText: "Search Status...", enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.white, ), // 👈 Normal border ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.white, width: 1.5, ), // 👈 Focused border ), ), ), ), onChanged: (val) { if (val != null) { setState(() { print("Selected Status: ${val['status_value']}"); selectedStatus = val['status_value']; }); } }, ), ), ], ); } Widget buildEndrosRemarks(context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Remarks *", style: _textStyle), SizedBox(height: 10), ThemedFormField( backgroundColor: Color(0xFFEDF6F5), controller: controllers['remarks']!, validator: (value) => Validators.requiredField(value, "remarks"), txtwidth: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.26, // keyboardType: TextInputType.multiline, // txtheight: 45, ), ], ); } Widget buildUploadDocumentEndorsment(context) { return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Upload Document", style: _textStyle), SizedBox(height: 10), ThemedUploadField( backgroundColor: Color(0xFFEDF6F5), hintText: selectedFileNames ?? "Upload Document", txtwidth: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.26, onFileSelected: (fileName, file) { setState(() { docUploadedFile = file; // This 'file' is a PlatformFile }); }, ), // Column( // mainAxisAlignment: MainAxisAlignment.end, // crossAxisAlignment: CrossAxisAlignment.end, // children: [ // ThemedUploadField( // hintText: selectedFileNames ?? "Upload Document", // txtwidth: MediaQuery.of(context).size.width * 0.26, // onFileSelected: (fileName, file) { // print("Picked file: $fileName (${file.size} bytes)"); // setState(() { // docUploadedEndorsFile = file; // }); // }, // ), // // const SizedBox(height: 5), // // // // if (docUploadedFile != null || passportEndorsFileUrlFromApi != null) // // Container( // // // color: Colors.white, // // child: Row( // // children: [ // // // // GestureDetector( // // onTap: () => apiService.downloadFile( // // apiUrl: // // 'agent/downloadAgentCertificateFile?agent_id=$selectedEndorsId', // // apiId: selectedEndorsId, // // localFile: docUploadedFile, // // fileName: selectedFileNames, // // ), // // child: Container( // // padding: const EdgeInsets.all(5), // // decoration: BoxDecoration( // // borderRadius: BorderRadius.circular(5), // // color: Color(0xFF425B5B), // // // color: Colors.green.shade300, // // ), // // child: Row( // // children: const [ // // Text( // // "Download", // // style: TextStyle( // // fontSize: 12, // // fontWeight: FontWeight.w200, // // color: Colors.white, // // ), // // ), // // SizedBox(width: 5), // // Icon(Icons.download, size: 13, color: Colors.white), // // ], // // ), // // ), // // ), // // ], // // ), // // ), // ], // ), ], ); } Widget buildContactPerson(context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Contact Person *", style: _textStyle), SizedBox(height: 10), ThemedFormField( backgroundColor: Color(0xFFEDF6F5), controller: controllers['contact_person']!, validator: (value) => Validators.requiredField(value, "contact_person"), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z]')), ], txtwidth: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.26, // keyboardType: TextInputType.multiline, // txtheight: 45, ), ], ); } Widget buildEndPremium(context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("End Premium *", style: _textStyle), SizedBox(height: 10), ThemedFormField( backgroundColor: Color(0xFFEDF6F5), controller: controllers['endorsement_premium']!, validator: (value) => Validators.requiredField(value, "endorsement_premium"), keyboardType: const TextInputType.numberWithOptions(decimal: true), /* * Allow decimal amount (e.g. 6000.00). * Integer-only formatter was removing "." and corrupting value. */ inputFormatters: decimalFormatter, txtwidth: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.26, ), ], ); } Widget buildPendingDays(context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Pending Days *", style: _textStyle), SizedBox(height: 10), ThemedFormField( backgroundColor: Color(0xFFEDF6F5), controller: controllers['pending_days']!, validator: (value) => Validators.requiredField(value, "pending_days"), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'[0-9]')), ], txtwidth: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.26, // keyboardType: TextInputType.multiline, // txtheight: 45, ), ], ); } Widget buildFinancialOrNonFinancial(context) { Map selectedEndorsementFNF = financialOptions.firstWhere( (item) => item['financial_value'].toString() == selectedFinancialStatus, orElse: () => {}, ); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Financial Status *", style: _textStyle), const SizedBox(height: 10), Container( color: Colors.white, width: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.26, child: DropdownSearch>( // Use a unique key for this dropdown key: const ValueKey("FinancialstatusDropdown"), selectedItem: selectedEndorsementFNF.isNotEmpty ? selectedEndorsementFNF : null, items: (filter, infiniteScrollProps) { return financialOptions; // Use the static list }, itemAsString: (val) => val['financial_value'].toString(), compareFn: (item, selectedItem) => item['financial_value'] == selectedItem['financial_value'], // --- REQUIRED VALIDATOR --- validator: (val) { if (val == null || val.isEmpty) { return "Required"; } return null; }, decoratorProps: DropDownDecoratorProps( decoration: AppInputDecorations.dropdownDecoration( label: "Select Financial Status", ).copyWith( filled: true, fillColor: Color( 0xFFEDF6F5,), // 👈 makes the dropdown input white isDense: true, // 👈 Makes the field compact contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 0), // 👈 Vertical 0 helps center the text ), ), popupProps: PopupProps.menu( fit: FlexFit.loose, constraints: const BoxConstraints(maxHeight: 150), // Shorter for 2 items menuProps: const MenuProps( backgroundColor: Colors.white, ), showSearchBox: true, // Disabled search as there are only 2 items searchFieldProps: TextFieldProps( decoration: InputDecoration( filled: true, fillColor: Colors.white, hintText: "Search Financial Status...", enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.white, ), // 👈 Normal border ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.white, width: 1.5, ), // 👈 Focused border ), ), ), ), onChanged: (val) { if (val != null) { setState(() { print("Selected Financial Status: ${val['financial_value']}"); selectedFinancialStatus = val['financial_value']; }); } }, ), ), ], ); } static final TextStyle _textStyle = TextStyle( fontSize: 12, fontWeight: FontWeight.w600, ); static final TextStyle _textStyle1 = TextStyle( fontSize: 9, fontWeight: FontWeight.w400, ); static final _dataBold = GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w400, color: Color(0xFF000000), ); static final _dataSub = GoogleFonts.inter( fontSize: 10, fontWeight: FontWeight.w300, color: Color(0xFF585757), ); static final _headerStyle = GoogleFonts.poppins( fontSize: 11.2, fontWeight: FontWeight.w500, color: Color(0xFF1E293B), ); static final _cardheaderStyle = GoogleFonts.inter( color: Colors.black, fontWeight: FontWeight.w600, fontSize: 12, ); static final _cardBodyStyle = GoogleFonts.inter( color: const Color(0xFF545454), fontWeight: FontWeight.w400, fontSize: 12, ); }