pre and post bug fix

This commit is contained in:
Surendiran 2026-07-03 11:45:37 +05:30
parent 3ec22aa137
commit a2d1480b5a
9 changed files with 1397 additions and 962 deletions

View File

@ -0,0 +1,98 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
class ClaimsOverviewCacheEntry {
final String policyId;
final Map<String, dynamic> claimsKpiBySlug;
final Map<String, dynamic> enrollmentKpiBySlug;
final DateTime generatedAt;
final String? claimsLoadError;
final String? enrollmentLoadError;
const ClaimsOverviewCacheEntry({
required this.policyId,
required this.claimsKpiBySlug,
required this.enrollmentKpiBySlug,
required this.generatedAt,
this.claimsLoadError,
this.enrollmentLoadError,
});
factory ClaimsOverviewCacheEntry.fromJson(Map<String, dynamic> json) {
return ClaimsOverviewCacheEntry(
policyId: json['policyId']?.toString() ?? '',
claimsKpiBySlug: Map<String, dynamic>.from(json['claimsKpiBySlug'] ?? {}),
enrollmentKpiBySlug:
Map<String, dynamic>.from(json['enrollmentKpiBySlug'] ?? {}),
generatedAt: DateTime.tryParse(json['generatedAt']?.toString() ?? '') ??
DateTime.now(),
claimsLoadError: json['claimsLoadError']?.toString(),
enrollmentLoadError: json['enrollmentLoadError']?.toString(),
);
}
Map<String, dynamic> toJson() => {
'policyId': policyId,
'claimsKpiBySlug': claimsKpiBySlug,
'enrollmentKpiBySlug': enrollmentKpiBySlug,
'generatedAt': generatedAt.toIso8601String(),
'claimsLoadError': claimsLoadError,
'enrollmentLoadError': enrollmentLoadError,
};
}
abstract final class ClaimsOverviewCache {
static const _keyPrefix = 'claims_overview_cache_v1_';
static String _key(String branchId, String policyId) =>
'$_keyPrefix${branchId}_$policyId';
static Future<ClaimsOverviewCacheEntry?> read(
String branchId,
String policyId,
) async {
if (branchId.isEmpty || policyId.isEmpty) return null;
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_key(branchId, policyId));
if (raw == null || raw.isEmpty) return null;
try {
final decoded = jsonDecode(raw);
if (decoded is! Map) return null;
return ClaimsOverviewCacheEntry.fromJson(
Map<String, dynamic>.from(decoded),
);
} catch (_) {
return null;
}
}
static Future<void> write(
String branchId,
ClaimsOverviewCacheEntry entry,
) async {
if (branchId.isEmpty || entry.policyId.isEmpty) return;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(
_key(branchId, entry.policyId),
jsonEncode(entry.toJson()),
);
}
static Future<void> clear(String branchId, String policyId) async {
if (branchId.isEmpty || policyId.isEmpty) return;
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_key(branchId, policyId));
}
static Future<void> clearAll() async {
final prefs = await SharedPreferences.getInstance();
final keys = prefs
.getKeys()
.where((key) => key.startsWith(_keyPrefix))
.toList();
for (final key in keys) {
await prefs.remove(key);
}
}
}

View File

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import '../../customAppBar/base_layout.dart';
import '../../customAppBar/toastHelper.dart';
@ -10,6 +11,7 @@ import '../../service/api_service.dart';
import '../../service/secure_pop_scope.dart';
import '../../service/token_storage_service.dart';
import 'claims_collection_kpi.dart';
import 'claims_overview_cache.dart';
import 'enrollment_collection_kpi.dart';
import 'claims_overview_animations.dart';
import 'claims_overview_pdf_export.dart';
@ -50,6 +52,13 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
String? _selectedPolicyId;
List<Map<String, dynamic>> _activePolicies = [];
Uint8List? _clientLogoBytes;
DateTime? _dataGeneratedAt;
String? get _formattedGeneratedAt {
final generatedAt = _dataGeneratedAt;
if (generatedAt == null) return null;
return DateFormat('d MMM yyyy, h:mm a').format(generatedAt.toLocal());
}
static const _tabs = [
(Icons.dashboard_outlined, 'Overview'),
@ -105,7 +114,10 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
});
}
Future<void> _loadDashboard({bool reloadPolicies = false}) async {
Future<void> _loadDashboard({
bool reloadPolicies = false,
bool forceRefresh = false,
}) async {
final isInitialLoad = _replayToken == 0;
setState(() {
if (isInitialLoad) {
@ -116,8 +128,8 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
});
try {
_clientId ??= await _tokenService.readValue('empClientId');
_clientBranchId ??= await _tokenService.readValue('empClientBranchId');
_clientId = await _tokenService.readValue('empClientId');
_clientBranchId = await _tokenService.readValue('empClientBranchId');
final hrId = await _tokenService.readValue('empHrId');
final token = await _tokenService.getCurrentToken();
if (token != null && token.isNotEmpty) {
@ -141,6 +153,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
error: 'No active policy found',
);
_enrollmentViewData = EnrollmentOverviewViewData.empty();
_dataGeneratedAt = null;
_isLoading = false;
_isRefreshing = false;
});
@ -148,6 +161,28 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
}
final policyId = _selectedPolicyId!;
final branchId = _clientBranchId ?? '';
if (forceRefresh) {
await ClaimsOverviewCache.clear(branchId, policyId);
}
if (!forceRefresh) {
final cached = await ClaimsOverviewCache.read(branchId, policyId);
if (cached != null) {
_applyCacheEntry(cached);
if (!mounted) return;
setState(() {
_isLoading = false;
_isRefreshing = false;
if (_replayToken == 0) {
_replayToken++;
}
});
return;
}
}
final response = await _apiService.getClaimsCollectionV2All(
clientPolicyId: policyId,
);
@ -186,6 +221,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
setState(() {
_viewData = claimsData;
_enrollmentViewData = enrollmentData;
_dataGeneratedAt = DateTime.now();
_isLoading = false;
_isRefreshing = false;
_replayToken++;
@ -195,10 +231,24 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
}
}
final generatedAt = DateTime.now();
await ClaimsOverviewCache.write(
branchId,
ClaimsOverviewCacheEntry(
policyId: policyId,
claimsKpiBySlug: claimsData.kpiBySlug,
enrollmentKpiBySlug: enrollmentData.kpiBySlug,
generatedAt: generatedAt,
claimsLoadError: claimsData.loadError,
enrollmentLoadError: enrollmentData.loadError,
),
);
if (!mounted) return;
setState(() {
_viewData = claimsData;
_enrollmentViewData = enrollmentData;
_dataGeneratedAt = generatedAt;
_isLoading = false;
_isRefreshing = false;
_replayToken++;
@ -209,6 +259,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
_viewData = ClaimsOverviewViewData.empty(error: e.toString());
_enrollmentViewData =
EnrollmentOverviewViewData.empty(error: e.toString());
_dataGeneratedAt = null;
_isLoading = false;
_isRefreshing = false;
});
@ -216,9 +267,21 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
}
}
void _applyCacheEntry(ClaimsOverviewCacheEntry entry) {
_viewData = ClaimsOverviewViewData(
kpiBySlug: Map<String, dynamic>.from(entry.claimsKpiBySlug),
loadError: entry.claimsLoadError,
);
_enrollmentViewData = EnrollmentOverviewViewData(
kpiBySlug: Map<String, dynamic>.from(entry.enrollmentKpiBySlug),
loadError: entry.enrollmentLoadError,
);
_dataGeneratedAt = entry.generatedAt;
}
Future<void> _loadPolicyList(String token, String hrId) async {
_clientId ??= await _tokenService.readValue('empClientId');
_clientBranchId ??= await _tokenService.readValue('empClientBranchId');
_clientId = await _tokenService.readValue('empClientId');
_clientBranchId = await _tokenService.readValue('empClientBranchId');
if (_clientId == null || _clientBranchId == null) return;
@ -386,7 +449,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
policyId: _selectedPolicyId ?? '',
dashboardInfo: ClaimsPdfDashboardInfo(
policyLabel: _selectedPolicyLabel(),
misCreationDate: _viewData.liveUpdatedAt,
misCreationDate: _formattedGeneratedAt,
),
clientLogoBytes: _clientLogoBytes,
onProgress: (_, __, label) {
@ -550,13 +613,17 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
color: ClaimsOverviewTheme.textSecondary,
),
),
if (_viewData.liveUpdatedAt != null &&
_viewData.liveUpdatedAt!.isNotEmpty) ...[
if (_formattedGeneratedAt != null) ...[
const SizedBox(width: 12),
ClaimsLiveIndicator(
createdAt: _viewData.liveUpdatedAt,
createdAt: _formattedGeneratedAt,
),
],
ClaimsRefreshIcon(
isRefreshing: _isRefreshing,
onPressed: () =>
_loadDashboard(forceRefresh: true),
),
],
),
],
@ -593,11 +660,6 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 4),
ClaimsRefreshIcon(
isRefreshing: _isRefreshing,
onPressed: _loadDashboard,
),
],
),
),
@ -639,10 +701,6 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
Icons.arrow_drop_down,
color: ClaimsOverviewTheme.textSecondary,
),
ClaimsRefreshIcon(
isRefreshing: _isRefreshing,
onPressed: _loadDashboard,
),
],
),
),

View File

@ -41,7 +41,7 @@ class ClaimsTabPanelRow extends StatelessWidget {
panels.map((p) {
return SizedBox(
width: available * p.flex / totalFlex,
child: ClipRect(child: p.child),
child: p.child,
);
}).toList(),
),

View File

@ -5,6 +5,15 @@ import 'claims_overview_animations.dart';
import 'claims_overview_scope.dart';
import 'claims_overview_theme.dart';
bool _overviewUsesWideLayout(
BuildContext context,
BoxConstraints constraints, {
double minWidth = 520,
}) {
if (ClaimsPdfExportScope.of(context)) return true;
return constraints.maxWidth >= minWidth;
}
/// Hover / tap focus wrapper for dashboard tiles.
class ClaimsFocusCard extends StatefulWidget {
final Widget child;
@ -163,9 +172,9 @@ class ClaimsPolicyInformationBody extends StatelessWidget {
return LayoutBuilder(
builder: (context, constraints) {
final narrow = constraints.maxWidth < 520;
final wide = _overviewUsesWideLayout(context, constraints);
if (narrow) {
if (!wide) {
return Column(
children: [
startTile,
@ -218,7 +227,7 @@ class ClaimsExperienceBody extends StatelessWidget {
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final wide = constraints.maxWidth >= 520;
final wide = _overviewUsesWideLayout(context, constraints);
final incurredTile = _OverviewInnerTile(
label: 'Incurred Claims',
icon: Icons.gps_fixed,
@ -283,7 +292,11 @@ class ClaimsInceptionBody extends StatelessWidget {
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final wide = constraints.maxWidth >= 400;
final wide = _overviewUsesWideLayout(
context,
constraints,
minWidth: 400,
);
final valueStyle = GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w700,
@ -519,7 +532,7 @@ class ClaimsPremiumMembershipBody extends StatelessWidget {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 520) {
if (!_overviewUsesWideLayout(context, constraints)) {
return Column(
children: [
premiumTile,

View File

@ -69,7 +69,7 @@ class _activePolicyExcelErrorState extends State<excelErrorScreen>
late ApiService apiService;
int _currentPage = 1;
int _rowsPerPage = 5;
int _rowsPerPage = 6;
List<List<Map<String, dynamic>>> get _paginatedExcelData {
final start = (_currentPage - 1) * _rowsPerPage;
@ -104,47 +104,45 @@ class _activePolicyExcelErrorState extends State<excelErrorScreen>
final response =
await apiService.getExcelFileErrorsApi(widget.id, widget.TokenType);
// 🔴 CASE 1: Empty data popup + back
if (response['data'] is List && response['data'].isEmpty) {
if (!hasExcelErrorTableData(response)) {
if (!mounted) return;
setState(() => isLoading = false);
_showEmptyDataDialog(response['message']);
_showEmptyDataDialog(
response['message']?.toString() ?? 'No error data available',
);
return;
}
// 🟢 CASE 2: Success with data
if (response['status'] == true) {
if (response['message'] == "Error data feteched successfully") {
// This runs if the message matches EXACTLY (including the typo 'feteched')
if (response['message'] == 'Error data feteched successfully') {
ToastHelper.showErrorToast(context, response['message']);
} else {
ToastHelper.showSuccessToast(context, response['message']);
}
if (!mounted) return;
setState(() {
isLoading = false;
isSuccess = false;
excelValidationStaus = 1;
excelHeader = List<String>.from(response['data']['excel_header']);
excelHeader =
List<String>.from(response['data']['excel_header'] as List);
excelData = (response['data']['excel_data'] as List)
.map<List<Map<String, dynamic>>>(
(row) => row
.map<Map<String, dynamic>>(
(cell) => Map<String, dynamic>.from(cell))
(cell) => Map<String, dynamic>.from(cell as Map),
)
.toList(),
)
.toList();
filteredExcelData = List.from(excelData);
_currentPage = 1;
});
}
// 🟡 CASE 3: API failed with message
else {
setState(() => isLoading = false);
_showEmptyDataDialog(response['message']);
}
} catch (e) {
if (!mounted) return;
setState(() => isLoading = false);
logDebug('Exception occurred: $e');
_showEmptyDataDialog('Something went wrong. Please try again.');
@ -152,30 +150,10 @@ class _activePolicyExcelErrorState extends State<excelErrorScreen>
}
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'),
),
],
);
},
showExcelErrorMessageDialog(
context,
message,
popRouteOnClose: true,
);
}
@ -295,18 +273,20 @@ class _activePolicyExcelErrorState extends State<excelErrorScreen>
}
Widget _buildContent(BuildContext context) {
return isLoading
? Container(
color: Color(0x98FFFCE5), // Semi-transparent background
if (isLoading || excelHeader.isEmpty) {
return Container(
color: const Color(0x98FFFCE5),
child: Center(
child: // Your GIF loader widget
Image.asset(
child: Image.asset(
height: 60,
width: 60,
'assets/nhance-loader.gif'), // Adjust path to your GIF loader
'assets/nhance-loader.gif',
),
)
: Container(
),
);
}
return Container(
// padding: EdgeInsets.only(top: 30, bottom: 200, left: 50, right: 50),
child: Column(
children: [
@ -442,7 +422,11 @@ class _activePolicyExcelErrorState extends State<excelErrorScreen>
}
Widget _buildCDDataTable(BuildContext context) {
return ScrollConfiguration(
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: ScrollConfiguration(
behavior: const MaterialScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
@ -451,19 +435,230 @@ class _activePolicyExcelErrorState extends State<excelErrorScreen>
},
),
child: _buildScrollableTable(context),
),
),
_buildPagination(context),
],
);
}
static const Map<String, double> _staticColumnWidthByHeader = {
'sno': 56,
'emp code': 128,
'name': 140,
'doj': 128,
'gender': 72,
'relation': 140,
'relationship': 140,
'dob': 152,
'mail': 220,
'mobile': 128,
'si': 96,
'grade': 96,
'basic pay': 100,
'unit': 80,
'doc': 128,
};
double _staticColumnWidth(String header) {
return _staticColumnWidthByHeader[header.trim().toLowerCase()] ?? 120;
}
Map<int, TableColumnWidth> get _tableColumnWidths {
return {
for (var i = 0; i < excelHeader.length; i++)
i: FixedColumnWidth(_staticColumnWidth(excelHeader[i])),
};
}
double get _totalTableWidth {
return excelHeader.fold<double>(
0,
(sum, header) => sum + _staticColumnWidth(header),
);
}
Widget _ellipsizedText(
String value, {
TextStyle? style,
}) {
final text = Text(
value,
style: style,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
);
if (value.isEmpty || value == '-') {
return text;
}
return Tooltip(
message: value,
waitDuration: const Duration(milliseconds: 300),
child: text,
);
}
Widget _tableHeaderCell(String header) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: _ellipsizedText(
header,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
);
}
Widget _tableDataCell(Map<String, dynamic> cell) {
final value = cell['value']?.toString() ?? '-';
final textStyle = GoogleFonts.poppins(fontSize: 12);
final hasError = cell.containsKey('error');
if (!hasError) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: _ellipsizedText(value, style: textStyle),
);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Row(
children: [
Expanded(
child: _ellipsizedText(value, style: textStyle),
),
const SizedBox(width: 4),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 24, minHeight: 24),
icon: const Icon(
Icons.error_outline,
color: Colors.red,
size: 16,
),
onPressed: () => _showCellErrorDialog(cell['error'] as List),
),
],
),
);
}
void _showCellErrorDialog(List errors) {
showDialog(
context: context,
barrierDismissible: true,
builder: (_) {
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: [
Text(
'Error!',
style: GoogleFonts.poppins(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 16),
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),
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),
...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),
),
),
),
),
const SizedBox(height: 20),
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,
),
),
),
),
],
),
),
);
},
);
}
Widget _buildScrollableTable(BuildContext context) {
const double columnWidth = 160;
final double tableWidth = excelHeader.length * columnWidth;
final tableWidth = _totalTableWidth;
return Scrollbar(
thumbVisibility: true,
controller: _verticalController,
child: SingleChildScrollView(
controller: _verticalController,
physics: const ClampingScrollPhysics(), // 👈 mouse wheel
physics: const ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
child: Scrollbar(
thumbVisibility: true,
@ -473,291 +668,150 @@ class _activePolicyExcelErrorState extends State<excelErrorScreen>
controller: _horizontalController,
physics: const ClampingScrollPhysics(),
scrollDirection: Axis.horizontal,
child: SizedBox(
width: tableWidth,
child: ConstrainedBox(
constraints: BoxConstraints.tightFor(width: tableWidth),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: const Color(0xFFD7E9EB),
ClipRRect(
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),
child: Table(
columnWidths: _tableColumnWidths,
defaultVerticalAlignment:
TableCellVerticalAlignment.middle,
children: [
TableRow(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: Color(0xFFD7E9EB),
),
children: excelHeader
.map(_tableHeaderCell)
.toList(),
),
],
),
),
const SizedBox(height: 6),
Table(
columnWidths: _tableColumnWidths,
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
border: const TableBorder(
horizontalInside: BorderSide(
color: Color(0xFFA9D9DE),
width: 1,
),
),
children: _paginatedExcelData.map((row) {
return TableRow(
children: List.generate(excelHeader.length, (index) {
final cell = index < row.length
? row[index]
: <String, dynamic>{'value': '-'};
return _tableDataCell(cell);
}),
);
}).toList(),
),
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(
Widget _buildPageButton(int page) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: _currentPage == page
? const Color(0xFF00A6A6)
: Colors.grey[300],
foregroundColor: _currentPage == page ? Colors.white : Colors.black,
minimumSize: const Size(36, 36),
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,
),
),
),
),
],
),
),
);
setState(() {
_currentPage = page;
});
},
);
},
),
],
)
: Text(
cell['value']?.toString() ?? '-',
style: GoogleFonts.poppins(
fontSize: 12,
),
),
),
);
}).toList(),
),
);
}).toList(),
/// PAGINATION
SizedBox(
width: tableWidth,
child: _buildPagination(context),
),
],
),
),
),
),
child: Text(page.toString()),
),
);
}
Widget _buildPagination(BuildContext context) {
final totalPages = (filteredExcelData.length / _rowsPerPage).ceil();
final totalItems = filteredExcelData.length;
final int startEntry =
totalItems == 0 ? 0 : ((_currentPage - 1) * _rowsPerPage) + 1;
int endEntry = _currentPage * _rowsPerPage;
if (endEntry > totalItems) endEntry = totalItems;
if (totalPages <= 1) {
return const SizedBox.shrink(); // 👈 hide if only one page
final totalPages =
totalItems == 0 ? 1 : (filteredExcelData.length / _rowsPerPage).ceil();
const visiblePageCount = 6;
List<int> getVisiblePages() {
if (totalPages <= visiblePageCount) {
return List.generate(totalPages, (i) => i + 1);
}
return Row(
mainAxisAlignment: MainAxisAlignment.end, // 👈 right aligned
if (_currentPage <= 3) {
return [1, 2, 3, 4, 5];
}
if (_currentPage >= totalPages - 2) {
return [
totalPages - 4,
totalPages - 3,
totalPages - 2,
totalPages - 1,
totalPages,
];
}
return [
_currentPage - 2,
_currentPage - 1,
_currentPage,
_currentPage + 1,
_currentPage + 2,
];
}
final visiblePages = getVisiblePages();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Showing $startEntry to $endEntry of $totalItems entries',
style: GoogleFonts.poppins(
fontSize: 13,
color: const Color(0xFF585757),
fontWeight: FontWeight.w400,
),
),
Row(
children: [
DropdownButton<int>(
value: _rowsPerPage,
items: [5, 10, 15, 20, 50].map((int value) {
items: [6, 10, 15, 20, 50].map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(
' $value ',
style: GoogleFonts.poppins(fontSize: 14),
style: GoogleFonts.poppins(fontSize: 15),
),
);
}).toList(),
onChanged: (newValue) {
onChanged: totalItems == 0
? null
: (newValue) {
setState(() {
_rowsPerPage = newValue!;
_currentPage = 1;
@ -765,38 +819,154 @@ class _activePolicyExcelErrorState extends State<excelErrorScreen>
},
),
IconButton(
onPressed: _currentPage > 1
? () => setState(() => _currentPage--)
: null,
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,
if (totalPages > 0 && !visiblePages.contains(1))
Row(
children: [
_buildPageButton(1),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: Text('...'),
),
onPressed: () {
setState(() {
_currentPage = i;
});
},
child: Text(i.toString()),
],
),
for (final page in visiblePages) _buildPageButton(page),
if (totalPages > 0 && !visiblePages.contains(totalPages))
Row(
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: Text('...'),
),
_buildPageButton(totalPages),
],
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: _currentPage < totalPages
? () => setState(() => _currentPage++)
: null,
icon: const Icon(Icons.chevron_right),
),
],
),
],
),
);
}
}
bool hasExcelErrorTableData(Map<String, dynamic> response) {
if (response['status'] != true) return false;
final data = response['data'];
if (data is! Map) return false;
final header = data['excel_header'];
return header is List && header.isNotEmpty;
}
Future<void> showExcelErrorMessageDialog(
BuildContext context,
String message, {
bool popRouteOnClose = false,
}) {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
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(dialogContext).pop();
if (popRouteOnClose && context.mounted) {
Navigator.of(context).pop();
}
},
child: const Text('Close'),
),
],
);
},
);
}
Future<void> openExcelErrorScreenIfAvailable({
required BuildContext context,
required ApiService apiService,
required String fileId,
required String tokenType,
required String clientId,
required String policyNo,
required String action,
required String createdAt,
required String clientBranchId,
required String token,
}) async {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => const PopScope(
canPop: false,
child: Center(child: CircularProgressIndicator()),
),
);
try {
final response = await apiService.getExcelFileErrorsApi(fileId, tokenType);
if (context.mounted) {
Navigator.of(context, rootNavigator: true).pop();
}
if (!hasExcelErrorTableData(response)) {
if (context.mounted) {
await showExcelErrorMessageDialog(
context,
response['message']?.toString() ?? 'No error data available',
);
}
return;
}
if (!context.mounted) return;
await Navigator.push<void>(
context,
MaterialPageRoute<void>(
builder: (context) => excelErrorScreen(
ClientId: clientId,
policy_no: policyNo,
action: action,
created_at: createdAt,
clientBranchId: clientBranchId,
Token: token,
TokenType: tokenType,
id: fileId,
),
),
);
} catch (e) {
if (context.mounted) {
final navigator = Navigator.of(context, rootNavigator: true);
if (navigator.canPop()) {
navigator.pop();
}
await showExcelErrorMessageDialog(
context,
'Something went wrong. Please try again.',
);
}
}
}

View File

@ -566,7 +566,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
case 'emp_count':
return relationship == 'self';
case 'enrolled':
return _isEnrolledStatus(status);
return _isSelfRow(row) && _isEnrolledStatus(status);
case 'not_enrolled':
return !_isEnrolledStatus(status);
case 'logged_in':
@ -574,7 +574,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
case 'not_logged_in':
return _isSelfRow(row) && !_isLoggedInRow(row);
case 'draft':
return status == 'draft';
return _isSelfRow(row) && status == 'draft';
default:
return true;
}
@ -1934,9 +1934,9 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
item['relationship']?.toString().toLowerCase().trim() ?? '';
if (relationship == 'self') empCount++;
if (_isEnrolledStatus(status)) {
if (_isSelfRow(item) && _isEnrolledStatus(status)) {
enrolled++;
} else {
} else if (!_isEnrolledStatus(status)) {
notEnrolled++;
}
if (_isSelfRow(item)) {
@ -1946,7 +1946,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
notLoggedIn++;
}
}
if (status == 'draft') draft++;
if (status == 'draft' && _isSelfRow(item)) draft++;
}
return {

View File

@ -900,38 +900,29 @@ class _postFileUploadState extends State<postFileUpload> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Select File Action
Expanded(
flex: 5,
child: buildStyledDropdown(
LayoutBuilder(
builder: (context, constraints) {
final narrow = constraints.maxWidth < 640;
final fileActionDropdown = buildStyledDropdown(
label: 'Select File Action',
value: selectedKey,
items: getFileUploadMasterList,
onChanged: (val) {
setState(() {
selectedKey = val;
final selectedItem = getFileUploadMasterList
.firstWhere((e) => e['key'] == val);
final selectedItem =
getFileUploadMasterList.firstWhere(
(e) => e['key'] == val,
);
selectedValue = selectedItem['value'];
currentApiValue = selectedItem['key'];
showSampleButton = true;
});
},
),
),
const SizedBox(width: 16),
/// Upload Box
Expanded(
flex: 5,
child: Column(
);
final uploadField = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// LABEL
RichText(
text: TextSpan(
text: 'Upload File',
@ -952,10 +943,7 @@ class _postFileUploadState extends State<postFileUpload> {
],
),
),
const SizedBox(height: 6),
/// DOTTED UPLOAD BOX
DragTarget<html.File>(
onAccept: (html.File droppedFile) {
setState(() {
@ -979,11 +967,11 @@ class _postFileUploadState extends State<postFileUpload> {
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(
horizontal: 12),
horizontal: 12,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(8),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFF00A6A6),
width: 1,
@ -995,8 +983,7 @@ class _postFileUploadState extends State<postFileUpload> {
child: Text(
fileName ??
'Upload Your Documents',
overflow:
TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 13,
color: fileName == null
@ -1012,13 +999,39 @@ class _postFileUploadState extends State<postFileUpload> {
),
],
),
));
),
);
},
),
],
);
if (narrow) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
fileActionDropdown,
const SizedBox(height: 16),
uploadField,
],
);
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 5,
child: fileActionDropdown,
),
const SizedBox(width: 16),
Expanded(
flex: 5,
child: uploadField,
),
],
);
},
),
SizedBox(height: 20),
Column(
@ -1146,15 +1159,21 @@ class _postFileUploadState extends State<postFileUpload> {
);
}
return LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final crossAxisCount = width < 640 ? 1 : 2;
const mainAxisExtent = 88.0;
return GridView.builder(
shrinkWrap: true, // IMPORTANT
physics: const NeverScrollableScrollPhysics(), // Disable inner scroll
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 10,
mainAxisExtent: mainAxisExtent,
),
itemCount: _paginatedData.length,
itemBuilder: (context, index) {
@ -1162,11 +1181,13 @@ class _postFileUploadState extends State<postFileUpload> {
return _buildFileCard(item);
},
);
},
);
}
Widget _buildFileCard(Map<String, dynamic> item) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFFEFF9FA),
borderRadius: BorderRadius.circular(12),
@ -1175,10 +1196,9 @@ class _postFileUploadState extends State<postFileUpload> {
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
/// 📄 File Icon
Container(
height: 44,
width: 44,
height: 40,
width: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
@ -1187,18 +1207,16 @@ class _postFileUploadState extends State<postFileUpload> {
child: const Icon(
Icons.description_outlined,
color: Color(0xFF00A6A6),
size: 22,
size: 20,
),
),
const SizedBox(width: 12),
/// 📑 LEFT CONTENT
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
/// Row 1 File name
Text(
item['file_name'] ?? '-',
maxLines: 1,
@ -1209,12 +1227,9 @@ class _postFileUploadState extends State<postFileUpload> {
color: const Color(0xFF101010),
),
),
const SizedBox(height: 4),
/// Row 2 Action - Date
RichText(
text: TextSpan(
const SizedBox(height: 2),
Text.rich(
TextSpan(
style: GoogleFonts.poppins(fontSize: 11),
children: [
TextSpan(
@ -1234,30 +1249,21 @@ class _postFileUploadState extends State<postFileUpload> {
),
],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
/// 👉 RIGHT SIDE (ICON + STATUS + DOWNLOAD)
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
/// 🔴 Error + Status
Row(
children: [],
),
const SizedBox(height: 8),
const SizedBox(width: 8),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (item['file_error_status'] == '1')
InkWell(
Tooltip(
message: 'Info',
child: InkWell(
onTap: () async {
logDebug(item);
// return;
final String? token =
await tokenService.getCurrentToken();
final String? empClientId =
@ -1265,75 +1271,60 @@ class _postFileUploadState extends State<postFileUpload> {
final String? empBranchId =
await tokenService.readValue('empClientBranchId');
logDebug(item);
logDebug(empClientId);
logDebug(localPolicyTypeId);
logDebug(empBranchId);
logDebug(token);
logDebug('post');
logDebug(localCardType);
logDebug(localCardPolicyNo);
logDebug(localCardInsurerName);
logDebug(localCardPolicyName);
logDebug(localCardPolicyExpDate);
logDebug(item['id']);
// SAFETY CHECK
if (token == null ||
empClientId == null ||
empBranchId == null) {
debugPrint(
'❌ Missing required data for navigation ${token}');
'❌ Missing required data for navigation $token');
return;
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => excelErrorScreen(
ClientId: empClientId,
policy_no: item['policy_no'],
action: item['file_action'],
created_at: item['created_at'],
if (!context.mounted) return;
await openExcelErrorScreenIfAvailable(
context: context,
apiService: apiService,
fileId: item['id'].toString(),
tokenType: 'post',
clientId: empClientId,
policyNo: item['policy_no']?.toString() ?? '',
action: item['file_action']?.toString() ?? '',
createdAt: item['created_at']?.toString() ?? '',
clientBranchId: empBranchId,
Token: token,
TokenType: 'post',
id: item['id']),
),
token: token,
);
},
child: Icon(
child: const Icon(
Icons.error,
size: 16,
color: Colors.red,
),
),
SizedBox(width: 10),
),
const SizedBox(width: 8),
_buildStatusChip(item['status']),
SizedBox(width: 10),
InkWell(
const SizedBox(width: 8),
Tooltip(
message: 'Download',
child: InkWell(
onTap: () {
getHrFileDownload(item['id'], item['file_name']);
},
child: Container(
height: 30,
width: 30,
height: 28,
width: 28,
decoration: BoxDecoration(
color: Color(0xFFC5F2F4),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF76CED2)),
),
child: Icon(
child: const Icon(
Icons.file_download_outlined,
color: Color(0xFF1D1B20),
size: 22,
size: 20,
),
),
),
],
),
/// Download
],
),
],
@ -1428,21 +1419,18 @@ class _postFileUploadState extends State<postFileUpload> {
List<int> visiblePages = getVisiblePages();
return Row(
mainAxisAlignment: MainAxisAlignment.end,
Widget paginationControls = Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
// Dropdown for rows per page
DropdownButton<int>(
value: _rowsPerPage,
items: [6, 10, 15, 20, 50].map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(' $value ',
style: GoogleFonts.poppins(fontSize: 15)),
child: Text(
' $value ',
style: GoogleFonts.poppins(fontSize: 15),
),
);
}).toList(),
onChanged: (newValue) {
@ -1452,40 +1440,33 @@ class _postFileUploadState extends State<postFileUpload> {
});
},
),
// Previous button
IconButton(
tooltip: 'Previous Page',
onPressed: _currentPage > 1
? () => setState(() => _currentPage--)
: null,
onPressed:
_currentPage > 1 ? () => setState(() => _currentPage--) : null,
icon: const Icon(Icons.chevron_left),
),
// First page + left ellipsis
if (!visiblePages.contains(1))
Row(children: [
Row(
children: [
_buildPageButton(1),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: Text("..."),
child: Text('...'),
),
],
),
]),
// Visible page buttons
for (int page in visiblePages) _buildPageButton(page),
// Right ellipsis + last page
if (!visiblePages.contains(totalPages))
Row(children: [
if (!visiblePages.contains(totalPages) && totalPages > 0)
Row(
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: Text("..."),
child: Text('...'),
),
_buildPageButton(totalPages),
]),
// Next button
],
),
IconButton(
onPressed: _currentPage < totalPages
? () => setState(() => _currentPage++)
@ -1493,12 +1474,56 @@ class _postFileUploadState extends State<postFileUpload> {
icon: const Icon(Icons.chevron_right),
),
],
);
final showingText = Text(
'Showing $startEntry to $endEntry of $totalItems entries',
style: GoogleFonts.poppins(
fontSize: 13,
color: const Color(0xFF585757),
fontWeight: FontWeight.w400,
),
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: LayoutBuilder(
builder: (context, constraints) {
final narrow = constraints.maxWidth < 720;
if (narrow) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
showingText,
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: paginationControls,
),
),
],
);
}
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
showingText,
Flexible(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: paginationControls,
),
),
],
);
},
),
);
}
Widget _buildPageButton(int page) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),

View File

@ -930,10 +930,77 @@ class _excelVerifyState extends State<preFileUpload> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
LayoutBuilder(
builder: (context, constraints) {
final narrow = constraints.maxWidth < 580;
if (narrow) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_dateField(
label: 'Enrolment Open Date',
controller: openDateController,
onTap: () async {
final picked = await showDatePicker(
context: context,
firstDate: DateTime(2000),
lastDate: DateTime.now(),
initialDate: DateTime.now(),
);
if (picked != null) {
final formatted =
DateFormat('dd-MM-yyyy')
.format(picked);
if (openDateController.text !=
formatted) {
closeDateController.clear();
}
openDateController.text = formatted;
}
},
),
const SizedBox(height: 16),
_dateField(
label: 'Enrolment Close Date',
controller: closeDateController,
onTap: () async {
if (openDateController.text.isEmpty) {
ToastHelper.showErrorToast(
context,
'Please select Enrolment Open Date first',
);
return;
}
final openDate =
DateFormat('dd-MM-yyyy').parse(
openDateController.text,
);
final picked = await showDatePicker(
context: context,
firstDate: openDate,
lastDate: DateTime(2100),
initialDate: openDate,
);
if (picked != null) {
closeDateController.text =
DateFormat('dd-MM-yyyy')
.format(picked);
}
},
),
],
);
}
return Row(
children: [
SizedBox(
width: 260, // 👈 set your required width
width: 260,
child: _dateField(
label: 'Enrolment Open Date',
controller: openDateController,
@ -946,10 +1013,11 @@ class _excelVerifyState extends State<preFileUpload> {
);
if (picked != null) {
final formatted =
DateFormat('dd-MM-yyyy').format(picked);
DateFormat('dd-MM-yyyy')
.format(picked);
// If open date changed, clear close date
if (openDateController.text != formatted) {
if (openDateController.text !=
formatted) {
closeDateController.clear();
}
@ -960,36 +1028,42 @@ class _excelVerifyState extends State<preFileUpload> {
),
const SizedBox(width: 16),
SizedBox(
width: 260, // 👈 same width
width: 260,
child: _dateField(
label: 'Enrolment Close Date',
controller: closeDateController,
onTap: () async {
if (openDateController.text.isEmpty) {
ToastHelper.showErrorToast(context,
'Please select Enrolment Open Date first');
ToastHelper.showErrorToast(
context,
'Please select Enrolment Open Date first',
);
return;
}
final openDate = DateFormat('dd-MM-yyyy')
.parse(openDateController.text);
final openDate =
DateFormat('dd-MM-yyyy').parse(
openDateController.text,
);
final picked = await showDatePicker(
context: context,
firstDate:
openDate, // Cannot select before open date
firstDate: openDate,
lastDate: DateTime(2100),
initialDate: openDate,
);
if (picked != null) {
closeDateController.text =
DateFormat('dd-MM-yyyy').format(picked);
DateFormat('dd-MM-yyyy')
.format(picked);
}
},
),
),
],
);
},
),
SizedBox(height: 20),
Row(
@ -1288,15 +1362,21 @@ class _excelVerifyState extends State<preFileUpload> {
return const Center(child: Text('No uploaded files'));
}
return LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final crossAxisCount = width < 640 ? 1 : 2;
const mainAxisExtent = 88.0;
return GridView.builder(
shrinkWrap: true, // IMPORTANT
physics: const NeverScrollableScrollPhysics(), // Disable inner scroll
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 10,
mainAxisExtent: mainAxisExtent,
),
itemCount: _paginatedData.length,
itemBuilder: (context, index) {
@ -1304,6 +1384,8 @@ class _excelVerifyState extends State<preFileUpload> {
return _buildFileCard(item);
},
);
},
);
}
// Widget _buildFileUploadedGrid() {
@ -1333,7 +1415,7 @@ class _excelVerifyState extends State<preFileUpload> {
Widget _buildFileCard(Map<String, dynamic> item) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFFEFF9FA),
borderRadius: BorderRadius.circular(12),
@ -1342,10 +1424,9 @@ class _excelVerifyState extends State<preFileUpload> {
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
/// 📄 File Icon
Container(
height: 44,
width: 44,
height: 40,
width: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
@ -1354,18 +1435,16 @@ class _excelVerifyState extends State<preFileUpload> {
child: const Icon(
Icons.description_outlined,
color: Color(0xFF00A6A6),
size: 22,
size: 20,
),
),
const SizedBox(width: 12),
/// 📑 LEFT CONTENT
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
/// Row 1 File name
Text(
item['file_name'] ?? '-',
maxLines: 1,
@ -1376,12 +1455,9 @@ class _excelVerifyState extends State<preFileUpload> {
color: const Color(0xFF101010),
),
),
const SizedBox(height: 4),
/// Row 2 Action - Date
RichText(
text: TextSpan(
const SizedBox(height: 2),
Text.rich(
TextSpan(
style: GoogleFonts.poppins(fontSize: 11),
children: [
TextSpan(
@ -1401,32 +1477,21 @@ class _excelVerifyState extends State<preFileUpload> {
),
],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
/// 👉 RIGHT SIDE (ICON + STATUS + DOWNLOAD)
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
/// 🔴 Error + Status
Row(
children: [],
),
const SizedBox(height: 8),
const SizedBox(width: 8),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (item['file_error_status'] == '1')
Tooltip(
message: 'Info', // Added tooltip name
message: 'Info',
child: InkWell(
onTap: () async {
logDebug(item);
// return;
final String? token =
await tokenService.getCurrentToken();
final String? enrollmentClient_id = await tokenService
@ -1435,81 +1500,62 @@ class _excelVerifyState extends State<preFileUpload> {
await tokenService
.readValue('enrollmentEmpClientBranchId');
logDebug(item);
logDebug(enrollmentClient_id);
logDebug(localPolicyTypeId);
logDebug(enrollmentEmpClientBranchId);
logDebug(token);
logDebug('post');
logDebug(localCardType);
logDebug(localCardPolicyNo);
logDebug(localCardInsurerName);
logDebug(localCardPolicyName);
logDebug(localCardPolicyExpDate);
logDebug(item['id']);
// SAFETY CHECK
if (token == null ||
enrollmentClient_id == null ||
enrollmentEmpClientBranchId == null) {
debugPrint(
'❌ Missing required data for navigation ${token}');
'❌ Missing required data for navigation $token');
return;
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => excelErrorScreen(
ClientId: enrollmentClient_id,
policy_no: item['policy_no'],
action: item['file_action'],
created_at: item['created_at'],
if (!context.mounted) return;
await openExcelErrorScreenIfAvailable(
context: context,
apiService: apiService,
fileId: item['id'].toString(),
tokenType: 'pre',
clientId: enrollmentClient_id,
policyNo: item['policy_no']?.toString() ?? '',
action: item['file_action']?.toString() ?? '',
createdAt: item['created_at']?.toString() ?? '',
clientBranchId: enrollmentEmpClientBranchId,
Token: token,
TokenType: 'pre',
id: item['id']),
),
token: token,
);
},
child: Icon(
child: const Icon(
Icons.error,
size: 16,
color: Colors.red,
),
),
),
SizedBox(width: 10),
const SizedBox(width: 8),
_buildStatusChip(item['status']),
SizedBox(width: 10),
const SizedBox(width: 8),
Tooltip(
message: 'Download', // Added tooltip name
message: 'Download',
child: InkWell(
onTap: () {
getHrFileDownload(item['id'], item['file_name']);
},
child: Container(
height: 30,
width: 30,
height: 28,
width: 28,
decoration: BoxDecoration(
color: Color(0xFFC5F2F4),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF76CED2)),
),
child: Icon(
child: const Icon(
Icons.file_download_outlined,
color: Color(0xFF1D1B20),
size: 22,
size: 20,
),
),
),
),
],
),
/// Download
],
),
],
),
);
@ -1586,35 +1632,18 @@ class _excelVerifyState extends State<preFileUpload> {
List<int> visiblePages = getVisiblePages();
return Padding(
// Match this horizontal padding (16) to your Table Header padding for perfect alignment
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment
.spaceBetween, // Pushes text to left, buttons to right
Widget paginationControls = Row(
mainAxisSize: MainAxisSize.min,
children: [
// --- LEFT SIDE: Showing Text ---
Text(
"Showing $startEntry to $endEntry of $totalItems entries",
style: GoogleFonts.poppins(
fontSize: 13,
color: const Color(0xFF585757),
fontWeight: FontWeight.w400,
),
),
// --- RIGHT SIDE: Controls ---
Row(
children: [
// Dropdown for rows per page
DropdownButton<int>(
value: _rowsPerPage,
// focusColor: Colors.transparent, // Fix: Removes the grey/blue highlight on change
items: [6, 10, 15, 20, 50].map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(' $value ',
style: GoogleFonts.poppins(fontSize: 15)),
child: Text(
' $value ',
style: GoogleFonts.poppins(fontSize: 15),
),
);
}).toList(),
onChanged: (newValue) {
@ -1624,39 +1653,32 @@ class _excelVerifyState extends State<preFileUpload> {
});
},
),
// Previous button
IconButton(
onPressed: _currentPage > 1
? () => setState(() => _currentPage--)
: null,
onPressed:
_currentPage > 1 ? () => setState(() => _currentPage--) : null,
icon: const Icon(Icons.chevron_left),
),
// First page + left ellipsis
if (!visiblePages.contains(1))
Row(children: [
Row(
children: [
_buildPageButton(1),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: Text("..."),
child: Text('...'),
),
],
),
]),
// Visible page buttons
for (int page in visiblePages) _buildPageButton(page),
// Right ellipsis + last page
if (!visiblePages.contains(totalPages) && totalPages > 0)
Row(children: [
Row(
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: Text("..."),
child: Text('...'),
),
_buildPageButton(totalPages),
]),
// Next button
],
),
IconButton(
onPressed: _currentPage < totalPages
? () => setState(() => _currentPage++)
@ -1664,8 +1686,52 @@ class _excelVerifyState extends State<preFileUpload> {
icon: const Icon(Icons.chevron_right),
),
],
);
final showingText = Text(
'Showing $startEntry to $endEntry of $totalItems entries',
style: GoogleFonts.poppins(
fontSize: 13,
color: const Color(0xFF585757),
fontWeight: FontWeight.w400,
),
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: LayoutBuilder(
builder: (context, constraints) {
final narrow = constraints.maxWidth < 720;
if (narrow) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
showingText,
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: paginationControls,
),
),
],
);
}
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
showingText,
Flexible(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: paginationControls,
),
),
],
);
},
),
);
}

View File

@ -1,6 +1,8 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../presentation/claims_overview/claims_overview_cache.dart';
import 'package:nhancepolicy/logger.dart';
class TokenStorageService {
@ -268,7 +270,10 @@ class TokenStorageService {
// 1 Clear ONLY branch/session related keys
await clearBranchSession();
// 2 Save selected branch
// 2 Clear claims overview cache so the dashboard fetches fresh data
await ClaimsOverviewCache.clearAll();
// 3 Save selected branch
await saveSelectedBranch(newBranch);
// 3 Rebuild decoded session data from token