768 lines
28 KiB
Dart
768 lines
28 KiB
Dart
import 'dart:ui';
|
||
|
||
import 'package:csv/csv.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:google_fonts/google_fonts.dart';
|
||
import 'package:intl/intl.dart';
|
||
import 'package:nhancepolicy/presentation/preFileUpload.dart';
|
||
import 'package:nhancepolicy/presentation/postFileUpload.dart';
|
||
import 'package:nhancepolicy/service/api_service.dart';
|
||
import 'dart:convert';
|
||
import 'dart:async';
|
||
import 'package:nhancepolicy/customAppBar/toastHelper.dart';
|
||
import 'package:nhancepolicy/service/token_storage_service.dart';
|
||
import 'package:universal_html/html.dart' as html;
|
||
import 'dart:typed_data';
|
||
import 'package:collection/collection.dart';
|
||
import 'package:url_launcher/url_launcher.dart';
|
||
import '../customAppBar/base_layout.dart';
|
||
|
||
class excelErrorScreen extends StatefulWidget {
|
||
final String ClientId;
|
||
final String policy_no;
|
||
final String action;
|
||
final String created_at;
|
||
final String clientBranchId;
|
||
final String Token;
|
||
final String TokenType;
|
||
final String id;
|
||
const excelErrorScreen({
|
||
Key? key,
|
||
required this.ClientId,
|
||
required this.policy_no,
|
||
required this.action,
|
||
required this.created_at,
|
||
required this.clientBranchId,
|
||
required this.Token,
|
||
required this.TokenType,
|
||
required this.id,
|
||
}) : super(key: key);
|
||
|
||
@override
|
||
State<excelErrorScreen> createState() => _activePolicyExcelErrorState();
|
||
}
|
||
|
||
class _activePolicyExcelErrorState extends State<excelErrorScreen>
|
||
with TickerProviderStateMixin {
|
||
final tokenService = TokenStorageService();
|
||
bool isLoading = false;
|
||
dynamic empPrimaryId;
|
||
dynamic empClientId;
|
||
dynamic empClientBranchId;
|
||
dynamic empHrId;
|
||
dynamic enrollmentClient_id;
|
||
dynamic enrollmentEmpClientBranchId;
|
||
dynamic enrollmentHrId;
|
||
TextEditingController searchController = TextEditingController();
|
||
List<String> excelHeader = [];
|
||
List<List<Map<String, dynamic>>> excelData = [];
|
||
|
||
late int excelValidationStaus = 1;
|
||
bool isSuccess = false;
|
||
String successContent = '';
|
||
|
||
List<List<Map<String, dynamic>>> filteredExcelData = [];
|
||
|
||
String? _postPreToken = '';
|
||
|
||
late ApiService apiService;
|
||
|
||
int _currentPage = 1;
|
||
int _rowsPerPage = 5;
|
||
|
||
List<List<Map<String, dynamic>>> get _paginatedExcelData {
|
||
final start = (_currentPage - 1) * _rowsPerPage;
|
||
final end =
|
||
(_currentPage * _rowsPerPage).clamp(0, filteredExcelData.length);
|
||
return filteredExcelData.sublist(start, end);
|
||
}
|
||
|
||
final ScrollController _verticalController = ScrollController();
|
||
final ScrollController _horizontalController = ScrollController();
|
||
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
apiService = ApiService(context);
|
||
getCDPoliciesDetails();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_verticalController.dispose();
|
||
_horizontalController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
|
||
Future<void> getCDPoliciesDetails() async {
|
||
setState(() {
|
||
isLoading = true;
|
||
});
|
||
|
||
try {
|
||
final response =
|
||
await apiService.getExcelFileErrorsApi(widget.id, widget.TokenType);
|
||
|
||
// 🔴 CASE 1: Empty data → popup + back
|
||
if (response['data'] is List && response['data'].isEmpty) {
|
||
setState(() => isLoading = false);
|
||
_showEmptyDataDialog(response['message']);
|
||
return;
|
||
}
|
||
|
||
// 🟢 CASE 2: Success with data
|
||
if (response['status'] == true) {
|
||
ToastHelper.showSuccessToast(context, response['message']);
|
||
|
||
setState(() {
|
||
isLoading = false;
|
||
isSuccess = false;
|
||
excelValidationStaus = 1;
|
||
|
||
excelHeader =
|
||
List<String>.from(response['data']['excel_header']);
|
||
|
||
excelData = (response['data']['excel_data'] as List)
|
||
.map<List<Map<String, dynamic>>>(
|
||
(row) => row
|
||
.map<Map<String, dynamic>>(
|
||
(cell) => Map<String, dynamic>.from(cell))
|
||
.toList(),
|
||
)
|
||
.toList();
|
||
|
||
filteredExcelData = List.from(excelData);
|
||
});
|
||
}
|
||
// 🟡 CASE 3: API failed with message
|
||
else {
|
||
setState(() => isLoading = false);
|
||
_showEmptyDataDialog(response['message']);
|
||
}
|
||
|
||
} catch (e) {
|
||
setState(() => isLoading = false);
|
||
print('Exception occurred: $e');
|
||
_showEmptyDataDialog('Something went wrong. Please try again.');
|
||
}
|
||
}
|
||
|
||
|
||
void _showEmptyDataDialog(String message) {
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (context) {
|
||
return AlertDialog(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
title: const Text(
|
||
'Message',
|
||
style: TextStyle(fontWeight: FontWeight.w600),
|
||
),
|
||
content: Text(message),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop(); // close dialog
|
||
Navigator.of(context).pop(); // go back page
|
||
},
|
||
child: const Text('Close'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
|
||
void search(String query) {
|
||
if (query.isEmpty) {
|
||
setState(() {
|
||
filteredExcelData = List.from(excelData);
|
||
_currentPage = 1;
|
||
});
|
||
return;
|
||
}
|
||
|
||
final lowerQuery = query.toLowerCase();
|
||
|
||
setState(() {
|
||
filteredExcelData = excelData.where((row) {
|
||
return row.any((cell) {
|
||
final value = cell['value'];
|
||
return value != null &&
|
||
value.toString().toLowerCase().contains(lowerQuery);
|
||
});
|
||
}).toList();
|
||
|
||
_currentPage = 1;
|
||
});
|
||
}
|
||
|
||
// emp_is_active
|
||
|
||
void exportToCsv({
|
||
required List<String> excelHeader,
|
||
required List<List<Map<String, dynamic>>> excelData,
|
||
}) {
|
||
List<List<String>> rows = [];
|
||
|
||
/// 1️⃣ Add headers
|
||
rows.add(excelHeader);
|
||
|
||
/// 2️⃣ Add rows
|
||
for (final row in excelData) {
|
||
rows.add(
|
||
row.map<String>((cell) {
|
||
final value = cell['value'];
|
||
return value == null ? '' : value.toString();
|
||
}).toList(),
|
||
);
|
||
}
|
||
|
||
/// 3️⃣ Convert to CSV
|
||
final csvData = const ListToCsvConverter().convert(rows);
|
||
|
||
/// 4️⃣ Download (Flutter Web)
|
||
final bytes = utf8.encode(csvData);
|
||
final blob = html.Blob([bytes], 'text/csv');
|
||
final url = html.Url.createObjectUrlFromBlob(blob);
|
||
|
||
html.AnchorElement(href: url)
|
||
..setAttribute("download", "Excel_Error_File.csv")
|
||
..click();
|
||
|
||
html.Url.revokeObjectUrl(url);
|
||
}
|
||
|
||
|
||
// Future<void> handleExportAction() async {
|
||
// print('handleExportAction');
|
||
//
|
||
// final postId = await tokenService.readValue('empHrId');
|
||
// final preId = await tokenService.readValue('enrollmentEmpPrimaryId');
|
||
//
|
||
// var activity = "export_empdata";
|
||
//
|
||
// var activityPre = "export_preempdata";
|
||
// dynamic response;
|
||
//
|
||
// print('postId - $postId');
|
||
// print('preId - $preId');
|
||
// print('activity - $activity');
|
||
//
|
||
// try {
|
||
// print('10');
|
||
//
|
||
// if (widget.TokenType == 'pre') {
|
||
// response = await apiService.getPreLogHrActivity(
|
||
// postId!, preId!, widget.Token, activityPre);
|
||
// } else if (widget.TokenType == 'post') {
|
||
// response = await apiService.getPostLogHrActivity(
|
||
// postId!, preId!, widget.Token, activity);
|
||
// }
|
||
//
|
||
// if (response['status'] == 'success') {
|
||
// print('Request success');
|
||
// } else {
|
||
// // ToastHelper.showWarningToast(
|
||
// // context, 'Request failed with status: ${response.statusCode}');
|
||
// print('Request failed with status: ${response['code']}');
|
||
// }
|
||
// } catch (e) {
|
||
// print('Exception occurred: $e');
|
||
// }
|
||
// }
|
||
|
||
String _capitalize(String? value) {
|
||
if (value == null || value.isEmpty) return '';
|
||
return value[0].toUpperCase() + value.substring(1).toLowerCase();
|
||
}
|
||
|
||
String formatDateTime(String dateTime) {
|
||
final parsedDate = DateTime.parse(dateTime);
|
||
return DateFormat('dd-MM-yyyy hh:mm a').format(parsedDate);
|
||
}
|
||
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return BaseLayout(
|
||
child: _buildContent(context),
|
||
);
|
||
}
|
||
|
||
Widget _buildContent(BuildContext context) {
|
||
return isLoading
|
||
? Container(
|
||
color: Color(0x98FFFCE5), // Semi-transparent background
|
||
child: Center(
|
||
child: // Your GIF loader widget
|
||
Image.asset(
|
||
height: 60,
|
||
width: 60,
|
||
'assets/nhance-loader.gif'), // Adjust path to your GIF loader
|
||
),
|
||
)
|
||
: Container(
|
||
// padding: EdgeInsets.only(top: 30, bottom: 200, left: 50, right: 50),
|
||
child: Column(
|
||
children: [
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
/// 🔙 Back + Title (LEFT)
|
||
Row(
|
||
children: [
|
||
IconButton(
|
||
onPressed: () => {Navigator.pop(context)},
|
||
icon: const Icon(
|
||
Icons.arrow_back_ios,
|
||
size: 18,
|
||
color: Colors.black,
|
||
),
|
||
padding: EdgeInsets.zero,
|
||
constraints: const BoxConstraints(),
|
||
),
|
||
const SizedBox(width: 6),
|
||
Container(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
widget.policy_no ?? '',
|
||
style: GoogleFonts.poppins(
|
||
color: Colors.black,
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
if (widget.TokenType != 'pre')
|
||
RichText(
|
||
text: TextSpan(
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w400,
|
||
),
|
||
children: [
|
||
TextSpan(
|
||
text: widget.action,
|
||
style: const TextStyle(
|
||
color: Color(0xFF00999E),
|
||
),
|
||
),
|
||
const TextSpan(
|
||
text: ' - ',
|
||
style: TextStyle(
|
||
color: Color(0xFF585858),
|
||
),
|
||
),
|
||
TextSpan(
|
||
text: formatDateTime(widget.created_at),
|
||
style: const TextStyle(
|
||
color: Color(0xFF585858),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
|
||
/// Push right content to end
|
||
const Spacer(),
|
||
|
||
/// 🔍 Search Box
|
||
Container(
|
||
width: 380,
|
||
height: 37,
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF0F0F0),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: TextField(
|
||
controller: searchController,
|
||
onChanged: search,
|
||
style: GoogleFonts.poppins(fontSize: 14),
|
||
decoration: const InputDecoration(
|
||
hintText: 'Search',
|
||
prefixIcon: Icon(Icons.search, size: 18),
|
||
border: InputBorder.none,
|
||
contentPadding:
|
||
EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
),
|
||
),
|
||
),
|
||
|
||
const SizedBox(width: 12),
|
||
|
||
/// ⬇️ Export Button
|
||
SizedBox(
|
||
width: 116,
|
||
height: 37,
|
||
child: ElevatedButton(
|
||
onPressed: () {
|
||
exportToCsv(
|
||
excelHeader: excelHeader,
|
||
excelData: filteredExcelData, // or excelData
|
||
);
|
||
|
||
},
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFFE26728),
|
||
elevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
),
|
||
child: Text(
|
||
'Export',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w700,
|
||
color: Colors.white,
|
||
letterSpacing: 1,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
SizedBox(height: 20),
|
||
Expanded(
|
||
child: _buildCDDataTable(context),
|
||
),
|
||
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildCDDataTable(BuildContext context) {
|
||
return ScrollConfiguration(
|
||
behavior: const MaterialScrollBehavior().copyWith(
|
||
dragDevices: {
|
||
PointerDeviceKind.mouse,
|
||
PointerDeviceKind.touch,
|
||
PointerDeviceKind.trackpad,
|
||
},
|
||
),
|
||
child: _buildScrollableTable(context),
|
||
);
|
||
}
|
||
|
||
Widget _buildScrollableTable(BuildContext context) {
|
||
const double columnWidth = 160;
|
||
final double tableWidth = excelHeader.length * columnWidth;
|
||
|
||
return Scrollbar(
|
||
thumbVisibility: true,
|
||
controller: _verticalController,
|
||
child: SingleChildScrollView(
|
||
controller: _verticalController,
|
||
physics: const ClampingScrollPhysics(), // 👈 mouse wheel
|
||
scrollDirection: Axis.vertical,
|
||
child: Scrollbar(
|
||
thumbVisibility: true,
|
||
controller: _horizontalController,
|
||
notificationPredicate: (n) => n.depth == 1,
|
||
child: SingleChildScrollView(
|
||
controller: _horizontalController,
|
||
physics: const ClampingScrollPhysics(),
|
||
scrollDirection: Axis.horizontal,
|
||
child: SizedBox(
|
||
width: tableWidth,
|
||
child: Column(
|
||
children: [
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFD7E9EB),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
child: Row(
|
||
children: excelHeader.map((header) {
|
||
return SizedBox(
|
||
width: columnWidth,
|
||
child: Padding(
|
||
padding:
|
||
const EdgeInsets.symmetric(horizontal: 12),
|
||
child: Text(
|
||
header,
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}).toList(),
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 6),
|
||
|
||
/// ROWS
|
||
..._paginatedExcelData.map((row) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
decoration: const BoxDecoration(
|
||
border: Border(
|
||
bottom: BorderSide(
|
||
color: Color(0xFFA9D9DE),
|
||
width: 1,
|
||
),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: row.map((cell) {
|
||
final bool hasError = cell.containsKey('error');
|
||
|
||
return SizedBox(
|
||
width: columnWidth,
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||
child: hasError
|
||
? Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
cell['value']?.toString() ?? '-',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 6),
|
||
IconButton(
|
||
padding: EdgeInsets.zero,
|
||
constraints: const BoxConstraints(),
|
||
icon: const Icon(
|
||
Icons.error_outline,
|
||
color: Colors.red,
|
||
size: 16,
|
||
),
|
||
onPressed: () {
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: true,
|
||
builder: (_) {
|
||
final List errors = cell['error'] as List;
|
||
|
||
return Dialog(
|
||
backgroundColor: Colors.transparent,
|
||
child: Container(
|
||
width: 420,
|
||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
/// ERROR TITLE
|
||
Text(
|
||
'Error!',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 32,
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.black,
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
/// RED ICON
|
||
Container(
|
||
width: 64,
|
||
height: 64,
|
||
decoration: const BoxDecoration(
|
||
color: Color(0xFFE0002A),
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: const Center(
|
||
child: Text(
|
||
'!',
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 36,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 20),
|
||
|
||
/// ERROR HEADING (optional – first error)
|
||
Text(
|
||
errors.isNotEmpty ? errors.first.toString() : 'Validation Error',
|
||
textAlign: TextAlign.center,
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.black,
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 12),
|
||
|
||
/// ERROR DETAILS
|
||
...errors.skip(1).map(
|
||
(e) => Padding(
|
||
padding: const EdgeInsets.only(top: 6),
|
||
child: Text(
|
||
e.toString(),
|
||
textAlign: TextAlign.center,
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 14,
|
||
color: const Color(0xFFE09B2D), // orange text
|
||
),
|
||
),
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 20),
|
||
|
||
/// OK BUTTON
|
||
SizedBox(
|
||
width: 120,
|
||
height: 30,
|
||
child: ElevatedButton(
|
||
onPressed: () => Navigator.pop(context),
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFFE0002A),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
elevation: 0,
|
||
),
|
||
child: Text(
|
||
'OK',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
|
||
},
|
||
),
|
||
],
|
||
)
|
||
: Text(
|
||
cell['value']?.toString() ?? '-',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}).toList(),
|
||
),
|
||
);
|
||
}).toList(),
|
||
|
||
/// PAGINATION
|
||
SizedBox(
|
||
width: tableWidth,
|
||
child: _buildPagination(context),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
|
||
Widget _buildPagination(BuildContext context) {
|
||
final totalPages =
|
||
(filteredExcelData.length / _rowsPerPage).ceil();
|
||
|
||
if (totalPages <= 1) {
|
||
return const SizedBox.shrink(); // 👈 hide if only one page
|
||
}
|
||
|
||
return Row(
|
||
mainAxisAlignment: MainAxisAlignment.end, // 👈 right aligned
|
||
children: [
|
||
DropdownButton<int>(
|
||
value: _rowsPerPage,
|
||
items: [5, 10, 15, 20, 50].map((int value) {
|
||
return DropdownMenuItem<int>(
|
||
value: value,
|
||
child: Text(
|
||
' $value ',
|
||
style: GoogleFonts.poppins(fontSize: 14),
|
||
),
|
||
);
|
||
}).toList(),
|
||
onChanged: (newValue) {
|
||
setState(() {
|
||
_rowsPerPage = newValue!;
|
||
_currentPage = 1;
|
||
});
|
||
},
|
||
),
|
||
|
||
IconButton(
|
||
icon: const Icon(Icons.chevron_left),
|
||
onPressed: _currentPage > 1
|
||
? () => setState(() => _currentPage--)
|
||
: null,
|
||
),
|
||
|
||
for (int i = 1; i <= totalPages; i++)
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||
child: ElevatedButton(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: _currentPage == i
|
||
? const Color(0xFF00A6A6)
|
||
: Colors.grey[300],
|
||
foregroundColor:
|
||
_currentPage == i ? Colors.white : Colors.black,
|
||
minimumSize: const Size(36, 36),
|
||
padding: EdgeInsets.zero,
|
||
),
|
||
onPressed: () {
|
||
setState(() {
|
||
_currentPage = i;
|
||
});
|
||
},
|
||
child: Text(i.toString()),
|
||
),
|
||
),
|
||
|
||
IconButton(
|
||
icon: const Icon(Icons.chevron_right),
|
||
onPressed: _currentPage < totalPages
|
||
? () => setState(() => _currentPage++)
|
||
: null,
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
|
||
}
|