2573 lines
82 KiB
Dart
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,
|
|
);
|
|
}
|