import 'package:dropdown_search/dropdown_search.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:intl/intl.dart'; import 'package:nhance_partner/presentation/providers/userRoleProvider.dart'; import '../../../../core/routing/routes.dart'; import '../../../../core/services/api_service.dart'; import '../../../../data/utils/Pagination.dart'; import '../../../../data/utils/toastNotification.dart'; import '../../../layouts/main_layout.dart'; import '../../../layouts/responsive_layout.dart'; import '../../../providers/manager_provider.dart'; import '../../../themes/indicators/customizd_file_upload.dart'; import '../../../themes/indicators/date_range_picker_field.dart'; import '../../../themes/indicators/export_btn.dart'; import '../../../themes/indicators/input_field_decoration.dart'; import '../../../themes/indicators/search_field_theme.dart'; import '../../../widgets/custom_action_popup.dart'; import '../enquiry/policy_popup.dart'; import '../enquiry/updateEndorsementDialog.dart'; import 'create_endorsement_dialog.dart'; class Endorsement extends ConsumerStatefulWidget { const Endorsement({super.key}); @override ConsumerState createState() => endosementState(); } class endosementState extends ConsumerState { int currentPage = 1; int itemsPerPage = 10; late ApiService apiService; // List> dataVal = []; List> getStaffData = []; List> originalData = []; List> filteredData = []; bool isLoading = false; dynamic userId; dynamic roleId; dynamic managerId; // bool isDrawerOpen = false; Map? selectedRow; List> endorsementNumbers = []; List> getEndrosmentType = []; List> filteredEndrosmentData = []; String? hoveredRowId; String? selectedFileNames; PlatformFile? docUploadedFile; String? lastPickedFile; @override void initState() { super.initState(); apiService = ApiService(); Future.microtask(() async { final id = ref.read(managerIdProvider); managerId = ref.read(managerIdProvider); roleId = ref.read(userRoleProvider); userId = ref.read(userIdProvider); print("F46 => r : $roleId | mId: $id | uId: $userId "); /// ⭐ DEFAULT FILTER FOR ACCOUNTS if (roleId == 'Accounts') { selectedVerificationVal = 0; // 0 = To Verify } else { selectedStatusVal = 'Open'; } if (userId != null) { await getStaffList(managerId, userId, roleId); } getEnroementType(); getInsurerDetails(); }); } Future getStaffList(int managerId, userId, role) async { setState(() { isLoading = true; }); print('fetchEndorsementList first $selectedVerificationVal'); try { final response = await apiService.fetchEndorsementList( managerId, userId, role, fromDate: startController.text.isNotEmpty ? DateFormat('yyyy-MM-dd') .format(DateFormat('dd-MM-yyyy') .parse(startController.text)) : null, toDate: endController.text.isNotEmpty ? DateFormat('yyyy-MM-dd') .format(DateFormat('dd-MM-yyyy') .parse(endController.text)) : null, endorsementType: selectedEndorsementTypeVal?.toString(), insurerId: selectedInsurerVal?.toString(), status: selectedStatusVal == 'All' ? '' : selectedStatusVal == 'Open' ? 'Open' : selectedStatusVal == 'Closed' ? 'Closed' : null, verification: selectedVerificationVal == 2 ? '' : selectedVerificationVal == 0 ? 'To Verify' : selectedVerificationVal == 1 ? 'Verified' : null, // selectedVerificationVal?.toString(), ); print('fetchEndorsementList'); if (response['status'] == 'success') { final data = response['data']; setState(() { if (data is List) { getStaffData = List>.from(data); } else if (data is Map) { getStaffData = [ Map.from(data) ]; } else { getStaffData = []; } originalData = getStaffData; filteredData = List.from(originalData); }); } else { getStaffData = []; originalData = []; } } catch (e) { print('Exception occurred: $e'); } finally { setState(() { isLoading = false; }); } } List get _paginatedData { // Sort descending by id first final sortedData = [...filteredData] ..sort((a, b) => int.parse(b['id']) - int.parse(a['id'])); if (sortedData.isEmpty) return []; // Ensure currentPage is valid final maxPage = (sortedData.length / itemsPerPage).ceil(); final safePage = currentPage.clamp(1, maxPage); final startIndex = (safePage - 1) * itemsPerPage; final endIndex = (startIndex + itemsPerPage).clamp(0, sortedData.length); return sortedData.sublist(startIndex, endIndex); } void refresh() { getStaffList(managerId, userId, roleId); } void filterData(String query) { final lowerQuery = query.toLowerCase(); setState(() { filteredData = getStaffData.where((item) { /// 🔹 Search all raw values final matchesRawValues = item.values.any( (value) => value != null && value.toString().toLowerCase().contains(lowerQuery), ); /// 🔹 Add computed verification text final verificationText = item['is_data_accuracy_checked'] == '0' ? 'to verify' : 'verified'; final matchesVerification = verificationText.contains(lowerQuery); return matchesRawValues || matchesVerification; }).toList(); }); } void _confirmDelete(BuildContext context, String id) { showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), title: Row( children: [ Icon(Icons.warning_amber_rounded, color: Colors.red.shade600, size: 22), const SizedBox(width: 8), Text( 'Delete Endorsement', style: GoogleFonts.poppins(fontSize: 15, fontWeight: FontWeight.w600), ), ], ), content: Text( 'Are you sure you want to delete this endorsement?\nThis action cannot be undone.', style: GoogleFonts.poppins(fontSize: 13, color: Colors.grey.shade700), ), actions: [ // CANCEL TextButton( onPressed: () => Navigator.pop(ctx), child: Text( 'Cancel', style: GoogleFonts.poppins(fontSize: 13, color: Colors.grey.shade600), ), ), // CONFIRM DELETE ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.red.shade600, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), onPressed: () async { Navigator.pop(ctx); await _deleteEndorsement(id); }, child: Text( 'Delete', style: GoogleFonts.poppins(fontSize: 13, color: Colors.white), ), ), ], ), ); } Future _deleteEndorsement(String id) async { try { setState(() => isLoading = true); final response = await apiService.deleteEndorsement(id); if (response['status'] == 'success' || response['code'] == 200) { refresh(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Endorsement deleted successfully.', style: GoogleFonts.poppins(fontSize: 13), ), backgroundColor: Colors.green.shade600, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), duration: const Duration(seconds: 2), ), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( response['message'] ?? 'Failed to delete endorsement.', style: GoogleFonts.poppins(fontSize: 13), ), backgroundColor: Colors.red.shade600, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), duration: const Duration(seconds: 2), ), ); } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: $e', style: GoogleFonts.poppins(fontSize: 13)), backgroundColor: Colors.red.shade600, behavior: SnackBarBehavior.floating, ), ); } finally { setState(() => isLoading = false); } } final TextEditingController rightSearchController = TextEditingController(); final TextEditingController startController = TextEditingController(); final TextEditingController endController = TextEditingController(); dynamic selectedStatusVal; dynamic selectedVerificationVal; dynamic selectedInsurerVal; dynamic selectedInsurer; dynamic selectedEndorsementTypeVal; List> getInsurerDetailsData = []; List> filteredInsurerData = []; bool isLoadingA = false; String _formatDate(String rawDate) { try { final dateTime = DateTime.parse(rawDate); return DateFormat('dd-MM-yyyy HH:mm').format(dateTime); // 24-hour format } catch (e) { return rawDate; // fallback if parsing fails } } String _truncateText(String? text, int maxLength) { if (text == null || text.length <= maxLength) { return text ?? '-'; } return '${text.substring(0, maxLength)}...'; } List _buildPopupMenuActions(BuildContext context, Map item) { final String Id = item['id']?.toString() ?? ''; final String? pdfPath = item["endorsement_completion_file"]; final String? fileName = (pdfPath != null && pdfPath.isNotEmpty) ? pdfPath.split('/').last : null; return [ // --- EDIT BUTTON --- InkWell( onTap: () { Navigator.pop(context); // Close the menu first showDialog( context: context, barrierDismissible: true, builder: (BuildContext dialogContext) { return UpdateEndorsementDialog( item: item, userId: userId, // Ensure these variables are accessible managerId: managerId, onSubmit: (val) => refresh(), ); }, ); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), child: Row( children: const [ Icon(Icons.edit_sharp, color: Color(0xFF319718), size: 18), SizedBox(width: 12), Text('Edit', style: TextStyle(fontSize: 14)), ], ), ), ), const Divider(height: 1, thickness: 0.5), // Subtle separator // --- DOWNLOAD BUTTON --- InkWell( onTap: () { Navigator.pop(context); // Close the menu first apiService.downloadFile( apiUrl: 'endorsement/downloadEndorsementCompletionFile?id=$Id', apiId: Id, localFile: null, fileName: fileName, ); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), child: Row( children: const [ Icon(Icons.download_rounded, color: Colors.blue, size: 18), SizedBox(width: 12), Text('Download', style: TextStyle(fontSize: 14)), ], ), ), ), // --- DELETE BUTTON --- if (roleId == 'Accounts') ...[ const Divider(height: 1, thickness: 0.5), InkWell( onTap: () { Navigator.pop(context); _confirmDelete(context, Id); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), child: Row( children: const [ Icon(Icons.delete_outline, color: Colors.red, size: 18), SizedBox(width: 12), Text('Delete', style: TextStyle(fontSize: 14, color: Colors.red)), ], ), ), ), ], ]; } Future fetchEndorsementExcelReport() async { final String searchValue = rightSearchController.text; await apiService.generateEndorsementExcel( managerId, searchValue: searchValue, fromDate: startController.text.isNotEmpty ? DateFormat('yyyy-MM-dd') .format(DateFormat('dd-MM-yyyy').parse(startController.text)) : '', toDate: endController.text.isNotEmpty ? DateFormat('yyyy-MM-dd') .format(DateFormat('dd-MM-yyyy').parse(endController.text)) : '', endorsementType: selectedEndorsementTypeVal?.toString() ?? '', insurerId: selectedInsurerVal?.toString() ?? '', status: selectedStatusVal == 'All' ? '' : selectedStatusVal == 'Open' ? 'Open' : selectedStatusVal == 'Closed' ? 'Closed' : '', verification: selectedVerificationVal == 2 ? '' : selectedVerificationVal == 0 ? 'To Verify' : selectedVerificationVal == 1 ? 'Verified' : '', policyNumber: '', ); // Map your controllers and IDs to the API parameters // final String fromDate = controllers['startDate']!.text; // "11-01-2025" // final String toDate = controllers['endDate']!.text; // "20-01-2025" // print("fetchPolicyExcelReport function called here."); // // try { // // Assuming you have a generic _makeGetRequest method // final response = await apiService.fetchPolicyExcel(managerId,fromDate,toDate); // // if (response != null && response['status'] == 200) { // setState(() { // // Store the list of data in your variable // excelResponseData = response['data']; // }); // print("Data fetched. Ready to export."); // } // } catch (e) { // debugPrint("Export Error: $e"); // print("Failed to fetch report data."); // } } 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; }); } } Future getInsurerDetails() async { setState(() => isLoadingA = true); try { final response = await apiService.fetchMasterDropDown('Insurers'); print("***999999999999999999*********** $response"); if (response['status'] == 200) { print('getInsurerDetails - ${response['data']}'); setState(() { getInsurerDetailsData = List>.from(response['data']); filteredInsurerData = List>.from(getInsurerDetailsData); print("Filtered Insurer Data: $filteredInsurerData"); }); } else { setState(() { getInsurerDetailsData = []; filteredInsurerData = []; }); } } catch (e) { print('Error: $e'); } finally { setState(() => isLoadingA = false); } } Future loadEndorsementNumbers(String id) async { // final response = await apiService.fetchEndorsementNumbers(id); // // if (response['status'] == 'success') { // setState(() { // endorsementNumbers = // List>.from(response['data']); // }); // } } @override Widget build(BuildContext context) { return MainLayout( title: "Endorsement", body: SelectionArea( child: Container( // color: Colors.yellow.shade50, width: MediaQuery.of(context).size.width, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ // Container( // height: 30, // width: MediaQuery.of(context).size.width, // child: GestureDetector( // onTap: () { // context.go(AppRoutes.dashboard); // }, // child: Row( // crossAxisAlignment: CrossAxisAlignment.center, // mainAxisAlignment: MainAxisAlignment.start, // children: [ // Tooltip( // message: 'Back', // child: IconButton( // icon: const Icon( // Icons.arrow_left_sharp, // size: 25, // color: Color(0xFF425B5B), // ), // onPressed: () { // context.go(AppRoutes.dashboard); // }, // splashRadius: 18, // hoverColor: Colors.black12, // padding: const EdgeInsets.all(4), // constraints: const BoxConstraints(), // ), // ), // const SizedBox(width: 5), // spacing between icon and text // Text( // "Endorsement", // style: GoogleFonts.poppins( // fontSize: 14, // fontWeight: FontWeight.w400, // ), // ), // ], // ), // ), // ), // // SizedBox(height: 5), Expanded( child: Container( // color: Colors.green, // color: Colors.green.shade50, width: MediaQuery.of(context).size.width, padding: EdgeInsets.all(8.0), child: Column( children: [ Container( // height: 40, // color: Colors.pink, child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ Text( "Endorsement", style: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.w400, ), ), ], ), ), SizedBox(height: 10), Container( width: double.infinity, child: Row( children: [ Expanded( child: buildFilterBar(context), ), ], ), ), SizedBox(height: 10), if (!ResponsiveLayout.isMobile(context)) Container( decoration: BoxDecoration( color: Color(0xFFF1F5F9), // color: Color(0xFFEDF6F5), borderRadius: BorderRadius.circular(6), ), padding: const EdgeInsets.symmetric( vertical: 8, horizontal: 16, ), child: Row( children: [ Expanded( flex: 1, child: Text('S No ', style: _headerStyle), ), Expanded( flex: 2, child: Text('Created Date ', style: _headerStyle), ), Expanded( flex: 3, child: Text('Endorsement Type ', style: _headerStyle), ), Expanded( flex: 2, child: Text('Vehicle No', style: _headerStyle), ), Expanded( flex: 2, child: Text('Insured Name', style: _headerStyle), ), Expanded( flex: 3, child: Text('Policy Number', style: _headerStyle), ), Expanded( flex: 2, child: Text('Start Date', style: _headerStyle), ), Expanded( flex: 2, child: Text( 'End Date', style: _headerStyle, ), ), Expanded( flex: 2, child: Text( 'Insurer', style: _headerStyle, ), ), Expanded( flex: 2, child: Text( 'Broker', style: _headerStyle, ), ), Expanded( flex: 2, child: Text( 'Partner', style: _headerStyle, ), ), Expanded( flex: 2, child: Text( 'Remarks', style: _headerStyle, ), ), Expanded( flex: 2, child: Text( 'Status', style: _headerStyle, ), ), // if(roleId == 'Accounts') Expanded( flex: 2, child: Text( 'Verification', style: _headerStyle, ), ), Expanded( flex: 2, child: Text('Pending Days', style: _headerStyle), ), Expanded( flex: 2, child: Text('Action', style: _headerStyle), ), ], ), ), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // MAIN TABLE Expanded( flex: 3, child: Container( // color: Colors.white, child: _buildDataTable(context), ), ), // DRAWER (only if open) // if (isDrawerOpen) // Expanded( // flex: 1, // child: _buildDrawer(), // ), ], ), ), ], ), ), ), Container( // height: 20, width: MediaQuery.of(context).size.width, // color: Colors.green.shade50, child: PaginationControls( currentPage: currentPage, itemsPerPage: itemsPerPage, // totalItems: dataVal.length, totalItems: filteredData.length, // activeColor: layoutColor, // your theme color onPageChanged: (page) { setState(() { currentPage = page; }); }, onItemsPerPageChanged: (items) { setState(() { itemsPerPage = items; currentPage = 1; }); }, ), ), ], ), ), ), ); } Widget buildFilterBar(BuildContext context) { return SizedBox( width: double.infinity, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFE5E7EB)), ), child: Row( children: [ /// 🔹 LEFT SIDE (SCROLLABLE FILTERS) Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ buildDateRangeFilter(context), const SizedBox(width: 12), buildEndorsementTypeSearch(context), const SizedBox(width: 12), buildSelectInsurer(context), const SizedBox(width: 12), buildStatusSearch(context), const SizedBox(width: 12), buildVerificationSearch(context), const SizedBox(width: 12), _iconButton(Icons.search, () { refresh(); }), const SizedBox(width: 8), _iconButton(Icons.refresh, () { startController.clear(); endController.clear(); selectedEndorsementTypeVal = null; selectedInsurerVal = null; selectedStatusVal = null; selectedVerificationVal = null; rightSearchController.clear(); refresh(); }), ], ), ), ), /// 🔹 RIGHT SIDE (FIXED) const SizedBox(width: 20), _rightSearchField(), const SizedBox(width: 12), if(roleId != 'Accounts')...[ _addButton(), const SizedBox(width: 10), ], _exportButton(), ], ), ), ); } Widget _buildDataTable(BuildContext context) { if (filteredData.isEmpty) { return const SizedBox( height: 50, child: Center(child: Text('No available data')), ); } final sortedData = [..._paginatedData]; return ListView.builder( itemCount: ResponsiveLayout.isMobile(context) ? sortedData .length // only cards for mobile : sortedData.length + 1, // +1 for header in desktop itemBuilder: (context, index) { if (!ResponsiveLayout.isMobile(context) && index == 0) { return _buildHeader(); } final startIndex = (currentPage - 1) * itemsPerPage; final item = sortedData[index - (ResponsiveLayout.isMobile(context) ? 0 : 1)]; final sno = startIndex + index; return !ResponsiveLayout.isMobile(context) ? _buildDataRow(item, sno) : _buildDataCard(item, sno); }, ); } Widget _buildHeader() { return SizedBox.shrink(); } Widget _buildDataRow(Map item, sno) { final String id = item['id']?.toString() ?? ''; final String? pdfPath = item["endorsement_completion_file"]; final String? fileName = pdfPath != null && pdfPath.isNotEmpty ? pdfPath.split('/').last : null; // final bool isSelected = selectedRow?['id']?.toString() == id; // final bool isHovered = hoveredRowId == id; // // Color bgColor = Colors.white; // // if (isSelected) { // bgColor = const Color(0xFFE6F4F1); // selected light teal // } else if (isHovered) { // bgColor = const Color(0xFFE6F4F1); // hover light grey // } return MouseRegion( cursor: SystemMouseCursors.basic, // Row-level tap gesture blocks SelectionArea text selection on web. // Keep this container non-gesture so users can select/copy text. child: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.symmetric( vertical: 10, horizontal: 16), decoration: BoxDecoration( // color: bgColor, border: const Border( bottom: BorderSide( color: Color(0xFFEAEAEA), width: 1, ), ), ), child: Row( children: [ Expanded(flex: 1, child: Text('$sno', style: _dataBold)), Expanded( flex: 2, child: Text( item['created_at'] != null ? item['created_at'].toString().split(' ')[0] : '-', style: _dataBold, ), ), Expanded( flex: 3, child: _buildBadge( text: item['endorsement_type_value'], type: 'endorsement', ), ), Expanded( flex: 2, child: Text(item['reg_no'] ?? '-', style: _dataBold)), Expanded( flex: 2, child: Text(item['insured_name'] ?? '-', style: _dataBold)), Expanded( flex: 3, child: Tooltip( message: _policyNumberHoverText( item['policy_number'], item['policy_from'], ), child: Text( item['policy_number'] ?? '-', style: _dataBold, overflow: TextOverflow.ellipsis, ), )), Expanded( flex: 2, child: Text(formatDateForList(item['policy_start_date']), style: _dataBold)), Expanded( flex: 2, child: Text(formatDateForList(item['policy_end_date']), style: _dataBold)), Expanded( flex: 2, child: Text(item['insurer_short_name'] ?? '-', style: _dataBold)), Expanded( flex: 2, child: Text(item['broker_name'] ?? '-', style: _dataBold)), Expanded( flex: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item['agent_code'] ?? '-', style: _dataBold), Text(_truncateText(item['agent_name'], 20), style: _dataSub), ], ), ), Expanded( flex: 2, child: Text( item['endorsement_description'] != null && item['endorsement_description'].toString().isNotEmpty ? item['endorsement_description'].toString() : '-', style: _dataBold, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Expanded( flex: 2, child: _buildBadge( text: item['status'], type: 'status', ), ), // if(roleId == 'Accounts') Expanded( flex: 2, child: _buildBadge( text: item['is_data_accuracy_checked'] == '0' ? 'To Verify' : 'Verified', type: 'verification', ), ), Expanded( flex: 2, child: Builder( builder: (context) { final days = _calculatePendingDays(item['created_at']?.toString()); return Center( child: Text( item['status'] == 'Closed' ? '-' : '$days', textAlign: TextAlign.center, style: _dataBold.copyWith( color: days > 7 ? Colors.red : days > 3 ? Colors.orange : Colors.green, ), ), ); }, ), ), Expanded( flex: 2, child: item['is_active'] == "1" ? Row( mainAxisSize: MainAxisSize.min, children: [ // --- EDIT --- _actionIconButton( context: context, tooltip: 'Edit', iconColor: const Color(0xFF319718), onTap: () async { final result = await context.push( AppRoutes.endorsomentValidation, extra: { "item": item, "title": "Endorsement Validation", "userId": userId, "managerId": managerId, "role": roleId, }, ); if (result == true) refresh(); }, customIcon: Image.asset( "assets/miscellaneous/Edit.png", height: 15, width: 15, ), ), // --- UPLOAD --- _actionIconButton( context: context, tooltip: 'Upload', iconColor: Colors.green.shade600, hoverColor: Colors.green.shade50, onTap: null, builderIcon: (buttonContext) => Material( color: Colors.transparent, child: Tooltip( message: 'Upload PDF', waitDuration: const Duration(milliseconds: 300), showDuration: const Duration(seconds: 2), child: InkWell( onTap: () => _showEndorsementUploadMenu(buttonContext, item), borderRadius: BorderRadius.circular(20), hoverColor: Colors.grey.shade200, child: Padding( padding: const EdgeInsets.all(5), child: Icon( Icons.file_upload_outlined, size: 15, color: Colors.green, ), ), ), ), ), ), // --- DOWNLOAD --- // --- DOWNLOAD BUTTON --- Builder( builder: (context) { final String? uploadedFile = item['endorsement_completion_file']; final String? uploadedFileName = (uploadedFile != null && uploadedFile.isNotEmpty) ? uploadedFile.split('/').last : null; if (uploadedFileName == null) return const SizedBox.shrink(); return _actionIconButton( context: context, icon: Icons.download_rounded, tooltip: 'Download Endorsement Document', iconColor: Colors.blue.shade600, hoverColor: Colors.blue.shade50, onTap: () => apiService.downloadFile( apiUrl: 'endorsement/downloadEndorsementCompletionFile?id=${item['id']}&type=completion', apiId: item['id'].toString(), localFile: null, fileName: uploadedFileName, ), ); }, ), // --- DELETE (Accounts only) --- if (roleId == 'Accounts') _actionIconButton( context: context, icon: Icons.delete_outline_rounded, tooltip: 'Delete', iconColor: Colors.red.shade600, hoverColor: Colors.red.shade50, // ✅ red tint on hover for delete onTap: () => _confirmDelete(context, id), ) ], ) : const Text('-'), ), ], ), ), ); } Widget _buildDataCard(Map item, int sno) { return Container( margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFF6FEFD), borderRadius: BorderRadius.circular(8.0), border: Border.all(color: const Color(0xffD9EBE8)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ //reg no Text(item['reg_no'] ?? '-', style: _headerStyle), PopupMenuButton( color: Colors.white, padding: EdgeInsets.zero, offset: Offset(0, 30), icon: Icon(Icons.more_vert, color: Color(0xFF475569), size: 14), itemBuilder: (context) => [ CustomPopupMenuEntry( child: Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: _buildPopupMenuActions(context, item), ), ), ), ], ), ], ), const Divider(color: Color(0xffD9EBE8), thickness: 0.8), // Policy From + Status Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ //date and time Text("policy From", style: _cardheaderStyle), Text( item['policy_from'] ?? '-', style: _cardBodyStyle, ), ], ), ), SizedBox(width: 5), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ //remarks Text("Status", style: _cardheaderStyle), Text( item['status'] ?? '-', style: _cardBodyStyle, maxLines: 3, softWrap: true, ), ], ), ), ], ), const SizedBox(height: 15), // Company + Vehicle Reg No Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ //company Text("Insurer", style: _cardheaderStyle), Text( item['insurer_name'] ?? '-', style: _cardBodyStyle, maxLines: 3, softWrap: true, ), ], ), ), SizedBox(width: 5), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ //status Text("Endorsement Type", style: _cardheaderStyle), Text( item['endorsement_type_value'] ?? '-', style: _cardBodyStyle, maxLines: 2, softWrap: true, ), ], ), ), ], ), const SizedBox(height: 15), // Created On + Remarks Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ //date and time Text("policy Number", style: _cardheaderStyle), Text( _formatDate(item['policy_number']) ?? '-', style: _cardBodyStyle, ), ], ), ), SizedBox(width: 5), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ //remarks Text("Endorsement Number", style: _cardheaderStyle), Text( item['endorsement_no'] ?? '-', style: _cardBodyStyle, maxLines: 3, softWrap: true, ), ], ), ), const SizedBox(height: 15), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Remarks", style: _cardheaderStyle), Text( item['endorsement_description'] != null && item['endorsement_description'].toString().isNotEmpty ? item['endorsement_description'].toString() : '-', style: _cardBodyStyle, maxLines: 3, softWrap: true, ), ], ), const SizedBox(height: 15), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Pending Days", style: _cardheaderStyle), Builder( builder: (context) { final days = _calculatePendingDays(item['created_at']?.toString()); return Text( item['status'] == 'Closed' ? '-' : '$days days', style: _cardBodyStyle.copyWith( color: days > 7 ? Colors.red : days > 3 ? Colors.orange : Colors.green, ), ); }, ), ], ), ], ), ], ), ); } Widget _buildDrawer() { return Container( margin: const EdgeInsets.only(left: 12), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: const Color(0xFFEAEAEA)), borderRadius: BorderRadius.circular(8), ), child: Column( children: [ // HEADER Container( padding: const EdgeInsets.all(12), decoration: const BoxDecoration( border: Border( bottom: BorderSide(color: Color(0xFFEAEAEA)), ), ), child: Row( children: [ Expanded( child: Text( "${selectedRow?['reg_no']} - Endorsements", style: _headerStyle, ), ), IconButton( icon: const Icon(Icons.close), onPressed: () { setState(() { // isDrawerOpen = false; selectedRow = null; }); }, ), ], ), ), // TABLE Expanded( child: endorsementNumbers.isEmpty ? const Center(child: Text("No endorsement numbers")) : ListView.builder( itemCount: endorsementNumbers.length, itemBuilder: (context, index) { final item = endorsementNumbers[index]; return Container( padding: const EdgeInsets.all(12), decoration: const BoxDecoration( border: Border( bottom: BorderSide( color: Color(0xFFEAEAEA), ), ), ), child: Row( children: [ Expanded( child: Text( item['endorsement_no'] ?? '-', style: _dataBold, ), ), IconButton( icon: const Icon(Icons.edit, size: 16), onPressed: () {}, ), IconButton( icon: const Icon(Icons.download, size: 16), onPressed: () {}, ), ], ), ); }, ), ), // PAGINATION PaginationControls( currentPage: 1, itemsPerPage: 10, totalItems: endorsementNumbers.length, onPageChanged: (page) {}, onItemsPerPageChanged: (items) {}, ), ], ), ); } Widget buildDateRangeFilter(BuildContext context) { return SizedBox( height: 36, width: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.15, child: DateRangePickerField( startController: startController, endController: endController, hintText: 'Select date range', showLabelAboveField: false, txtheight: 36, lastDate: DateTime.now(), useFormField: false, ), ); } Widget buildEndorsementTypeSearch(BuildContext context) { final List> endorsementOptions = List>.from(filteredEndrosmentData); Map? selectedItem; if (selectedEndorsementTypeVal != null && selectedEndorsementTypeVal.toString().isNotEmpty && endorsementOptions.isNotEmpty) { try { selectedItem = endorsementOptions.firstWhere( (e) => e['id'].toString() == selectedEndorsementTypeVal.toString(), ); } catch (_) { selectedItem = null; } } return SizedBox( height: 33, child: Container( width: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.10, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFE2E8F0)), ), child: DropdownSearch>( selectedItem: selectedItem, items: (filter, infiniteScrollProps) { if (filter.isEmpty) return endorsementOptions; return endorsementOptions .where((item) => item['endorsement_type'] .toString() .toLowerCase() .contains(filter.toLowerCase())) .toList(); }, itemAsString: (item) => item['endorsement_type']?.toString() ?? '', compareFn: (item, selected) { if (selected == null) return false; return item['id'].toString() == selected['id'].toString(); }, dropdownBuilder: (context, selectedItem) { return Align( alignment: Alignment.centerLeft, child: Text( selectedItem != null ? selectedItem['endorsement_type'] .toString() : '', style: GoogleFonts.poppins( fontSize: 11, color: Colors.black, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ); }, decoratorProps: DropDownDecoratorProps( decoration: AppInputDecorations.dropdownDecoration( label: "Endorsement Type", ).copyWith( filled: true, fillColor: Colors.white, isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 5, vertical: 1, ), ), ), popupProps: PopupProps.menu( showSearchBox: true, fit: FlexFit.loose, constraints: BoxConstraints( maxHeight: 300, minWidth: ResponsiveLayout.isMobile(context) ? 200 : MediaQuery.of(context).size.width * 0.15, ), menuProps: const MenuProps(backgroundColor: Colors.white), ), onChanged: (val) { if (val == null) return; selectedEndorsementTypeVal = val['id']; // refresh(); }, ), ), ); } Widget buildSelectInsurer(BuildContext context) { // Ensure this list has data BEFORE building dropdown final List> insurerOptions = List>.from(filteredInsurerData); // Selected item logic Map? selectedItem; if (selectedInsurerVal != null && selectedInsurerVal.toString().isNotEmpty && insurerOptions.isNotEmpty) { try { selectedItem = insurerOptions.firstWhere( (e) => e['id'].toString() == selectedInsurerVal.toString(), ); } catch (e) { selectedItem = null; } } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 33, child: Container( width: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.10, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10 ), border: Border.all(color: const Color(0xFFE2E8F0)), ), child: DropdownSearch>( selectedItem: selectedItem, // FILTER LOGIC: This filters the list as the user types items: (filter, infiniteScrollProps) { if (filter.isEmpty) { return insurerOptions; } return insurerOptions .where((item) => item['short_name'] .toString() .toLowerCase() .contains(filter.toLowerCase())) .toList(); }, itemAsString: (item) => item['short_name']?.toString() ?? '', compareFn: (item, selected) { if (selected == null) return false; return item['id'].toString() == selected['id'].toString(); }, // FIELD UI (The collapsed state) dropdownBuilder: (context, selectedItem) { return Align( alignment: Alignment.centerLeft, child: Text( selectedItem != null ? selectedItem['short_name'].toString() : '', style: GoogleFonts.poppins( fontSize: 11, color: Colors.black, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ); }, decoratorProps: DropDownDecoratorProps( decoration: AppInputDecorations.dropdownDecoration( label: "Select Insurer", ).copyWith( filled: true, fillColor: Colors.white, isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 5, vertical: 1, ), ), ), // POPUP UI (The expanded state with search) popupProps: PopupProps.menu( showSearchBox: true, // Enabled searching fit: FlexFit.loose, constraints: BoxConstraints( maxHeight: 300, minWidth: ResponsiveLayout.isMobile(context) ? 200 : MediaQuery.of(context).size.width * 0.10, ), menuProps: const MenuProps(backgroundColor: Colors.white), // Customizing the search field appearance searchFieldProps: TextFieldProps( style: GoogleFonts.poppins(fontSize: 13), decoration: InputDecoration( hintText: "Search insurer...", hintStyle: GoogleFonts.poppins(fontSize: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), ), ), itemBuilder: (context, item, isDisabled, isSelected) { return Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), decoration: BoxDecoration( color: isSelected ? Colors.blue.withOpacity(0.1) : Colors.transparent, ), child: Text( item['short_name'].toString(), style: GoogleFonts.inter( fontSize: 13, color: Colors.black, ), ), ); }, ), // ON CHANGE onChanged: (val) { if (val == null) return; selectedInsurerVal = val['id']; // refresh(); }, ), ), ), ], ); } Widget buildStatusSearch(BuildContext context) { final List> statusOptions = [ {'id': 3, 'status': 'All'}, {'id': 1, 'status': 'Open'}, {'id': 2, 'status': 'Closed'}, ]; Map? selectedItem; if (selectedStatusVal != null) { try { selectedItem = statusOptions.firstWhere( (e) => e['status'] == selectedStatusVal, ); } catch (_) { selectedItem = null; } } return SizedBox( height: 33, child: Container( width: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.10, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFE2E8F0)), ), child: DropdownSearch>( selectedItem: selectedItem, items: (filter, infiniteScrollProps) => statusOptions, itemAsString: (val) => val['status'].toString(), compareFn: (item, selected) { if (selected == null) return false; return item['id'] == selected['id']; }, dropdownBuilder: (context, selectedItem) { return Align( alignment: Alignment.centerLeft, child: Text( selectedItem != null ? selectedItem['status'].toString() : '', style: GoogleFonts.poppins( fontSize: 11, color: Colors.black, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ); }, decoratorProps: DropDownDecoratorProps( decoration: AppInputDecorations.dropdownDecoration( label: "Select Status", ).copyWith( filled: true, fillColor: Colors.white, isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 5, vertical: 1, ), ), ), popupProps: PopupProps.menu( showSearchBox: false, fit: FlexFit.loose, constraints: BoxConstraints( maxHeight: 300, minWidth: ResponsiveLayout.isMobile(context) ? 200 : MediaQuery.of(context).size.width * 0.10, ), menuProps: const MenuProps(backgroundColor: Colors.white), ), onChanged: (val) { if (val == null) return; selectedStatusVal = val['status']; // refresh(); }, ), ), ); } Widget buildVerificationSearch(BuildContext context) { final List> verificationOptions = [ {'id': 2, 'status': 'All'}, {'id': 0, 'status': 'To Verify'}, {'id': 1, 'status': 'Verified'}, ]; Map? selectedItem; if (selectedVerificationVal != null) { try { selectedItem = verificationOptions.firstWhere( (e) => e['id'] == selectedVerificationVal, ); } catch (_) { selectedItem = null; } } return SizedBox( height: 33, child: Container( width: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.10, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFE2E8F0)), ), child: DropdownSearch>( selectedItem: selectedItem, items: (filter, infiniteScrollProps) => verificationOptions, itemAsString: (val) => val['status'].toString(), compareFn: (item, selected) { if (selected == null) return false; return item['id'] == selected['id']; }, dropdownBuilder: (context, selectedItem) { return Align( alignment: Alignment.centerLeft, child: Text( selectedItem != null ? selectedItem['status'].toString() : '', style: GoogleFonts.poppins( fontSize: 11, color: Colors.black, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ); }, decoratorProps: DropDownDecoratorProps( decoration: AppInputDecorations.dropdownDecoration( label: "Select Verification", ).copyWith( filled: true, fillColor: Colors.white, isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 5, vertical: 1, ), ), ), popupProps: PopupProps.menu( showSearchBox: false, fit: FlexFit.loose, constraints: BoxConstraints( maxHeight: 300, minWidth: ResponsiveLayout.isMobile(context) ? 200 : MediaQuery.of(context).size.width * 0.10, ), menuProps: const MenuProps(backgroundColor: Colors.white), ), onChanged: (val) { if (val == null) return; selectedVerificationVal = val['id']; // refresh(); }, ), ), ); } Widget _iconButton( IconData icon, VoidCallback? onTap) { return InkWell( onTap: onTap, child: Container( width: 36, height: 36, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all( color: const Color(0xFFE5E7EB)), color: Colors.white, ), child: Icon(icon, size: 18, color: Colors.grey[700]), ), ); } Widget _rightSearchField() { return Container( width: 220, height: 36, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFE5E7EB)), color: Colors.white, ), child: Row( children: [ const Icon(Icons.search, size: 18, color: Colors.grey), const SizedBox(width: 8), Expanded( child: TextField( controller: rightSearchController, decoration: InputDecoration( hintText: "Search...", border: InputBorder.none, isDense: true, hintStyle: GoogleFonts.poppins(fontSize: 11), ), style: GoogleFonts.poppins(fontSize: 11), onChanged: (val) { filterData(val); }, ), ), ], ), ); } Widget _exportButton() { return InkWell( onTap: () async { await fetchEndorsementExcelReport(); }, child: Container( width: 40, height: 36, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: const Color(0xFF2E7D6E), ), child:Tooltip( message: 'Export', child: Image.asset( "assets/miscellaneous/export.png", height: 13, width: 13, ), ), ), ); } Widget _addButton() { return InkWell( onTap: () async { showDialog( context: context, builder: (ctx) { final screenWidth = MediaQuery.of(ctx).size.width; double dialogWidth; if (screenWidth > 1400) { dialogWidth = 1200; } else if (screenWidth > 1100) { dialogWidth = 1000; } else if (screenWidth > 900) { dialogWidth = 850; } else { dialogWidth = screenWidth * 0.95; // mobile/tablet } return Center( child: SizedBox( width: dialogWidth, child: CreateEndorsementDialog( managerId: managerId, userId: userId, role: roleId, title: "Endorsement", onSubmit: (value) { debugPrint("New Endorsement: $value"); refresh(); }, ), ), ); }, ); }, child: Container( width: 40, height: 36, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: const Color(0xFF2E7D6E), ), child: const Tooltip( message: 'Create Endorsement', child: Icon( Icons.add, color: Colors.white, size: 18, ), ), ), ); } Widget _actionIconButton({ required BuildContext context, IconData? icon, Widget? customIcon, Widget Function(BuildContext buttonContext)? builderIcon, required String tooltip, required Color iconColor, required VoidCallback? onTap, Color? hoverColor, // ✅ Add this parameter }) { if (builderIcon != null) { return Builder(builder: (buttonContext) => builderIcon(buttonContext)); } return Material( color: Colors.transparent, child: Tooltip( message: tooltip, waitDuration: const Duration(milliseconds: 300), showDuration: const Duration(seconds: 2), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(20), hoverColor: hoverColor ?? Colors.grey.shade200, // ✅ default grey child: Padding( padding: const EdgeInsets.all(5), child: customIcon ?? Icon(icon, size: 15, color: iconColor), ), ), ), ); } // Widget _editButton(item) { // return InkWell( // onTap: () async { // showDialog( // context: context, // builder: (ctx) { // final screenWidth = MediaQuery.of(ctx).size.width; // // double dialogWidth; // // if (screenWidth > 1400) { // dialogWidth = 1200; // } else if (screenWidth > 1100) { // dialogWidth = 1000; // } else if (screenWidth > 900) { // dialogWidth = 850; // } else { // dialogWidth = screenWidth * 0.95; // mobile/tablet // } // // return Center( // child: SizedBox( // width: dialogWidth, // child: endorsomentUpdateValidation( // item:item, // managerId: managerId, // userId: userId, // role: roleId, // title: "Endorsement", // onSubmit: (value) { // debugPrint("New Endorsement: $value"); // refresh(); // }, // ), // ), // ); // }, // ); // }, // child: Container( // width: 40, // height: 36, // decoration: BoxDecoration( // borderRadius: BorderRadius.circular(8), // color: const Color(0xFF2E7D6E), // ), // child: const Tooltip( // message: 'Create Endorsement', // child: Icon( // Icons.add, // color: Colors.white, // size: 18, // ), // ), // ), // ); // } Widget _buildBadge({ required String? text, required String type, }) { String value = text ?? '-'; Color bgColor = Colors.grey.shade200; Color textColor = Colors.black87; // 🔹 STATUS COLORS if (type == 'status') { switch (value) { case 'Open': bgColor = const Color(0xFFFDECC8); textColor = const Color(0xFFB45309); break; case 'Closed': bgColor = const Color(0xFFD1FADF); textColor = const Color(0xFF067647); break; } } // 🔹 VERIFICATION COLORS else if (type == 'verification') { switch (value) { case 'To Verify': bgColor = const Color(0xFFE0EAFF); textColor = const Color(0xFF1D4ED8); break; case 'Verified': bgColor = const Color(0xFFD1FADF); textColor = const Color(0xFF067647); break; } } // 🔹 ENDORSEMENT TYPE COLORS (Purple Style) else if (type == 'endorsement') { bgColor = const Color(0xFFEDE9FE); // light purple textColor = const Color(0xFF6D28D9); // purple } return Align( alignment: Alignment.centerLeft, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 5, ), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(20), ), child: Text( value, style: GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w500, color: textColor, ), ), ), ); } /* * Hover text format: * "INHY/00009887 (internal)" or "0123489945666 (external)". */ String _policyNumberHoverText(dynamic policyNumber, dynamic policyFrom) { final String number = (policyNumber?.toString().trim().isNotEmpty ?? false) ? policyNumber.toString().trim() : '-'; final String source = policyFrom?.toString().trim().toLowerCase() ?? ''; if (source == 'internal' || source == 'external') { return '$number ($source)'; } return number; } int _calculatePendingDays(String? createdAt) { if (createdAt == null || createdAt.isEmpty) return 0; try { final created = DateFormat('dd-MM-yyyy').parse(createdAt.split(' ')[0]); final today = DateTime.now(); return today.difference(created).inDays; } catch (e) { return 0; } } String formatDateForList(String? dateStr) { /* * Normalize all non-usable date values for UI. * Required output: "-" for Internal/Invalid/null/empty/zero-range dates. */ if (dateStr == null) return '-'; final String normalized = dateStr.trim(); if (normalized.isEmpty || normalized == '-') return '-'; if (normalized.toLowerCase() == 'internal') return '-'; if (normalized.toLowerCase() == 'invalid date') return '-'; try { /* * Catch zero/invalid ranges: * - 0000-00-00 * - dd-00-yyyy / dd-mm-0000 * - malformed historical invalid strings. */ if (normalized.contains('0000') || normalized.contains('-00-') || normalized.startsWith('00-') || normalized.endsWith('-00') || normalized.startsWith('30-11--')) { return '-'; } DateTime parsed; /* * API commonly sends date as dd-MM-yyyy (e.g. 05-12-2025), * while some flows may send ISO (yyyy-MM-dd). Support both. */ if (RegExp(r'^\d{2}-\d{2}-\d{4}$').hasMatch(normalized)) { parsed = DateFormat('dd-MM-yyyy').parseStrict(normalized); } else { parsed = DateTime.parse(normalized); } // Validate year is reasonable if (parsed.year < 2000 || parsed.year > 2100) return '-'; return DateFormat('dd-MM-yyyy').format(parsed); } catch (e) { return '-'; } } Future _showEndorsementUploadMenu( BuildContext buttonContext, Map item, ) async { final button = buttonContext.findRenderObject() as RenderBox; final overlay = Overlay.of(buttonContext).context.findRenderObject() as RenderBox; final position = button.localToGlobal(Offset.zero, ancestor: overlay); await showMenu( context: buttonContext, position: RelativeRect.fromLTRB( position.dx, position.dy + button.size.height, overlay.size.width, 0, ), items: [ PopupMenuItem( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text('Upload Endorsement Document (PDF)', style: _dataBold), const SizedBox(height: 10), _buildEndorsementUploadField(item), ], ), ), ], color: Colors.white, ); } // Helper: Endorsement upload field Widget _buildEndorsementUploadField(Map item) { final String? uploadedFile = item['endorsement_completion_file']; final String? uploadedFileName = (uploadedFile != null && uploadedFile.isNotEmpty) ? uploadedFile.split('/').last : null; return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // ── UPLOAD LABEL ────────────────────────────── // Text('Upload Endorsement Document', style: GoogleFonts.poppins(fontSize: 11, fontWeight: FontWeight.w500)), const SizedBox(height: 6), // ── UPLOAD FIELD ────────────────────────────── ThemedUploadField( hintText: selectedFileNames ?? "Upload Endorsement Document", padHorizontal: 4, padVertical: 5, fontSZ: 11, borderCirculr: 5, txtwidth: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.18, borderColor: const Color(0xFFE2E8F0), onFileSelected: (fileName, file) => _handleEndorsementFileUpload(fileName, file, item), ), const SizedBox(height: 10), // ── ALREADY UPLOADED FILE ROW ───────────────── if (uploadedFileName != null) ...[ Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: const Color(0xFFF0FDF4), borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFFBBF7D0)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.picture_as_pdf, color: Colors.red, size: 16), const SizedBox(width: 6), Flexible( child: Text( uploadedFileName, style: GoogleFonts.poppins(fontSize: 11, color: Colors.black87), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), const SizedBox(width: 8), // ── DOWNLOAD BUTTON ────────────────── Material( color: Colors.transparent, child: InkWell( onTap: () { apiService.downloadFile( apiUrl: 'endorsement/downloadEndorsementCompletionFile?id=${item['id']}&type=completion', apiId: item['id'].toString(), localFile: null, fileName: uploadedFileName, ); }, borderRadius: BorderRadius.circular(20), hoverColor: Colors.blue.shade50, // ✅ hover color child: Padding( padding: const EdgeInsets.all(4), child: Icon( Icons.download_rounded, color: Colors.blue, // ✅ icon color size: 16, ), ), ), ), ], ), ), ] else ...[ // ── NO FILE MESSAGE ─────────────────────── Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: const Color(0xFFFFF7ED), borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFFFED7AA)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.info_outline, color: Colors.orange.shade400, size: 14), const SizedBox(width: 6), Text( 'No Endorsement file uploaded yet', style: GoogleFonts.poppins(fontSize: 11, color: Colors.orange.shade700), ), ], ), ), ], ], ); } // Helper: Handle Endorsement file upload Future _handleEndorsementFileUpload( String? fileName, PlatformFile? file, Map item, ) async { // ✅ Guard: userId must exist if (userId == null) { ToastHelper.showErrorToast(context, 'User session not found. Please re-login.'); return; } // ✅ Guard: item id must exist if (item["id"] == null) { ToastHelper.showErrorToast(context, 'Invalid endorsement record.'); return; } if (lastPickedFile == fileName) { ToastHelper.showErrorToast(context, 'Please upload a new file.'); setState(() { selectedFileNames = null; docUploadedFile = null; }); return; } if (file == null || fileName == null || fileName.isEmpty || file.size == 0) { return; } if (!fileName.toLowerCase().endsWith('.pdf')) { ToastHelper.showErrorToast(context, 'Only PDF files are allowed.'); return; } lastPickedFile = fileName; setState(() { selectedFileNames = fileName; docUploadedFile = file; }); await uploadEndorsementPDF( file: file, // ✅ use local var, not state var data: { "updated_by": userId, "id": item["id"], }, ); if (context.mounted) Navigator.of(context).pop(); } Future uploadEndorsementPDF({ PlatformFile? file, required Map data, }) async { if (file == null) return; try { setState(() => isLoading = true); final response = await apiService.uploadEndorsementFile( file: file, data: data, ); if (response['status'] == 'success' || response['code'] == 200) { setState(() { selectedFileNames = null; docUploadedFile = null; lastPickedFile = null; }); refresh(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'File uploaded successfully.', style: GoogleFonts.poppins(fontSize: 13), ), backgroundColor: Colors.green.shade600, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), duration: const Duration(seconds: 2), ), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( response['message'] ?? 'Upload failed.', style: GoogleFonts.poppins(fontSize: 13), ), backgroundColor: Colors.red.shade600, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), ); } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: $e', style: GoogleFonts.poppins(fontSize: 13)), backgroundColor: Colors.red.shade600, behavior: SnackBarBehavior.floating, ), ); } finally { setState(() => isLoading = false); } } 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, ); }