nhance_partner/lib/presentation/screens/Enquiry/policy_claims_endros/endrosment.dart
2026-04-18 17:36:06 +05:30

2573 lines
82 KiB
Dart

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<Endorsement> createState() => endosementState();
}
class endosementState extends ConsumerState<Endorsement> {
int currentPage = 1;
int itemsPerPage = 10;
late ApiService apiService;
// List<Map<String, dynamic>> dataVal = [];
List<Map<String, dynamic>> getStaffData = [];
List<Map<String, dynamic>> originalData = [];
List<Map<String, dynamic>> filteredData = [];
bool isLoading = false;
dynamic userId;
dynamic roleId;
dynamic managerId;
// bool isDrawerOpen = false;
Map<String, dynamic>? selectedRow;
List<Map<String, dynamic>> endorsementNumbers = [];
List<Map<String, dynamic>> getEndrosmentType = [];
List<Map<String, dynamic>> 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<void> 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<Map<String, dynamic>>.from(data);
} else if (data is Map) {
getStaffData = [
Map<String, dynamic>.from(data)
];
} else {
getStaffData = [];
}
originalData = getStaffData;
filteredData = List.from(originalData);
});
} else {
getStaffData = [];
originalData = [];
}
} catch (e) {
print('Exception occurred: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
List<dynamic> 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<void> _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<Map<String, dynamic>> getInsurerDetailsData = [];
List<Map<String, dynamic>> 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<Widget> _buildPopupMenuActions(BuildContext context, Map<String, dynamic> 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<void> 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<void> 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<Map<String, dynamic>>.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<void> 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<Map<String, dynamic>>.from(response['data']);
filteredInsurerData =
List<Map<String, dynamic>>.from(getInsurerDetailsData);
print("Filtered Insurer Data: $filteredInsurerData");
});
} else {
setState(() {
getInsurerDetailsData = [];
filteredInsurerData = [];
});
}
} catch (e) {
print('Error: $e');
} finally {
setState(() => isLoadingA = false);
}
}
Future<void> loadEndorsementNumbers(String id) async {
// final response = await apiService.fetchEndorsementNumbers(id);
//
// if (response['status'] == 'success') {
// setState(() {
// endorsementNumbers =
// List<Map<String, dynamic>>.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<String, dynamic> 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<String, dynamic> 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<int>(
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<Map<String, dynamic>> endorsementOptions =
List<Map<String, dynamic>>.from(filteredEndrosmentData);
Map<String, dynamic>? 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<Map<String, dynamic>>(
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<Map<String, dynamic>> insurerOptions =
List<Map<String, dynamic>>.from(filteredInsurerData);
// Selected item logic
Map<String, dynamic>? 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<Map<String, dynamic>>(
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<Map<String, dynamic>> statusOptions = [
{'id': 3, 'status': 'All'},
{'id': 1, 'status': 'Open'},
{'id': 2, 'status': 'Closed'},
];
Map<String, dynamic>? 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<Map<String, dynamic>>(
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<Map<String, dynamic>> verificationOptions = [
{'id': 2, 'status': 'All'},
{'id': 0, 'status': 'To Verify'},
{'id': 1, 'status': 'Verified'},
];
Map<String, dynamic>? 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<Map<String, dynamic>>(
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<void> _showEndorsementUploadMenu(
BuildContext buttonContext,
Map<String, dynamic> 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<String, dynamic> 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<void> _handleEndorsementFileUpload(
String? fileName,
PlatformFile? file,
Map<String, dynamic> 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<void> uploadEndorsementPDF({
PlatformFile? file,
required Map<String, dynamic> 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,
);
}