diff --git a/lib/presentation/claims_overview/claims_overview_cache.dart b/lib/presentation/claims_overview/claims_overview_cache.dart new file mode 100644 index 0000000..cf84f23 --- /dev/null +++ b/lib/presentation/claims_overview/claims_overview_cache.dart @@ -0,0 +1,98 @@ +import 'dart:convert'; + +import 'package:shared_preferences/shared_preferences.dart'; + +class ClaimsOverviewCacheEntry { + final String policyId; + final Map claimsKpiBySlug; + final Map 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 json) { + return ClaimsOverviewCacheEntry( + policyId: json['policyId']?.toString() ?? '', + claimsKpiBySlug: Map.from(json['claimsKpiBySlug'] ?? {}), + enrollmentKpiBySlug: + Map.from(json['enrollmentKpiBySlug'] ?? {}), + generatedAt: DateTime.tryParse(json['generatedAt']?.toString() ?? '') ?? + DateTime.now(), + claimsLoadError: json['claimsLoadError']?.toString(), + enrollmentLoadError: json['enrollmentLoadError']?.toString(), + ); + } + + Map 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 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.from(decoded), + ); + } catch (_) { + return null; + } + } + + static Future 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 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 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); + } + } +} diff --git a/lib/presentation/claims_overview/claims_overview_dashboard.dart b/lib/presentation/claims_overview/claims_overview_dashboard.dart index 9a9a13c..ba340dc 100644 --- a/lib/presentation/claims_overview/claims_overview_dashboard.dart +++ b/lib/presentation/claims_overview/claims_overview_dashboard.dart @@ -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 String? _selectedPolicyId; List> _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 }); } - Future _loadDashboard({bool reloadPolicies = false}) async { + Future _loadDashboard({ + bool reloadPolicies = false, + bool forceRefresh = false, + }) async { final isInitialLoad = _replayToken == 0; setState(() { if (isInitialLoad) { @@ -116,8 +128,8 @@ class _ClaimsOverviewDashboardState extends State }); 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 error: 'No active policy found', ); _enrollmentViewData = EnrollmentOverviewViewData.empty(); + _dataGeneratedAt = null; _isLoading = false; _isRefreshing = false; }); @@ -148,6 +161,28 @@ class _ClaimsOverviewDashboardState extends State } 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 setState(() { _viewData = claimsData; _enrollmentViewData = enrollmentData; + _dataGeneratedAt = DateTime.now(); _isLoading = false; _isRefreshing = false; _replayToken++; @@ -195,10 +231,24 @@ class _ClaimsOverviewDashboardState extends State } } + 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 _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 } } + void _applyCacheEntry(ClaimsOverviewCacheEntry entry) { + _viewData = ClaimsOverviewViewData( + kpiBySlug: Map.from(entry.claimsKpiBySlug), + loadError: entry.claimsLoadError, + ); + _enrollmentViewData = EnrollmentOverviewViewData( + kpiBySlug: Map.from(entry.enrollmentKpiBySlug), + loadError: entry.enrollmentLoadError, + ); + _dataGeneratedAt = entry.generatedAt; + } + Future _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 policyId: _selectedPolicyId ?? '', dashboardInfo: ClaimsPdfDashboardInfo( policyLabel: _selectedPolicyLabel(), - misCreationDate: _viewData.liveUpdatedAt, + misCreationDate: _formattedGeneratedAt, ), clientLogoBytes: _clientLogoBytes, onProgress: (_, __, label) { @@ -550,13 +613,17 @@ class _ClaimsOverviewDashboardState extends State 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 fontWeight: FontWeight.w600, ), ), - const SizedBox(width: 4), - ClaimsRefreshIcon( - isRefreshing: _isRefreshing, - onPressed: _loadDashboard, - ), ], ), ), @@ -639,10 +701,6 @@ class _ClaimsOverviewDashboardState extends State Icons.arrow_drop_down, color: ClaimsOverviewTheme.textSecondary, ), - ClaimsRefreshIcon( - isRefreshing: _isRefreshing, - onPressed: _loadDashboard, - ), ], ), ), diff --git a/lib/presentation/claims_overview/claims_overview_pdf_layout.dart b/lib/presentation/claims_overview/claims_overview_pdf_layout.dart index 432851b..5b039ae 100644 --- a/lib/presentation/claims_overview/claims_overview_pdf_layout.dart +++ b/lib/presentation/claims_overview/claims_overview_pdf_layout.dart @@ -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(), ), diff --git a/lib/presentation/claims_overview/claims_overview_widgets.dart b/lib/presentation/claims_overview/claims_overview_widgets.dart index de660ae..6520805 100644 --- a/lib/presentation/claims_overview/claims_overview_widgets.dart +++ b/lib/presentation/claims_overview/claims_overview_widgets.dart @@ -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, diff --git a/lib/presentation/excelVerification.dart b/lib/presentation/excelVerification.dart index 1c755f4..d49d9c2 100644 --- a/lib/presentation/excelVerification.dart +++ b/lib/presentation/excelVerification.dart @@ -69,7 +69,7 @@ class _activePolicyExcelErrorState extends State late ApiService apiService; int _currentPage = 1; - int _rowsPerPage = 5; + int _rowsPerPage = 6; List>> get _paginatedExcelData { final start = (_currentPage - 1) * _rowsPerPage; @@ -104,47 +104,45 @@ class _activePolicyExcelErrorState extends State 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') - ToastHelper.showErrorToast(context, response['message']); - } else { - ToastHelper.showSuccessToast(context, response['message']); - } - - setState(() { - isLoading = false; - isSuccess = false; - excelValidationStaus = 1; - - excelHeader = List.from(response['data']['excel_header']); - - excelData = (response['data']['excel_data'] as List) - .map>>( - (row) => row - .map>( - (cell) => Map.from(cell)) - .toList(), - ) - .toList(); - - filteredExcelData = List.from(excelData); - }); - } - // 🟡 CASE 3: API failed with message - else { - setState(() => isLoading = false); - _showEmptyDataDialog(response['message']); + 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.from(response['data']['excel_header'] as List); + + excelData = (response['data']['excel_data'] as List) + .map>>( + (row) => row + .map>( + (cell) => Map.from(cell as Map), + ) + .toList(), + ) + .toList(); + + filteredExcelData = List.from(excelData); + _currentPage = 1; + }); } 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 } 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 } 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( + if (isLoading || excelHeader.isEmpty) { + return Container( + color: const Color(0x98FFFCE5), + child: Center( + child: Image.asset( + height: 60, + width: 60, + 'assets/nhance-loader.gif', + ), + ), + ); + } + + return Container( // padding: EdgeInsets.only(top: 30, bottom: 200, left: 50, right: 50), child: Column( children: [ @@ -442,28 +422,243 @@ class _activePolicyExcelErrorState extends State } Widget _buildCDDataTable(BuildContext context) { - return ScrollConfiguration( - behavior: const MaterialScrollBehavior().copyWith( - dragDevices: { - PointerDeviceKind.mouse, - PointerDeviceKind.touch, - PointerDeviceKind.trackpad, - }, + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ScrollConfiguration( + behavior: const MaterialScrollBehavior().copyWith( + dragDevices: { + PointerDeviceKind.mouse, + PointerDeviceKind.touch, + PointerDeviceKind.trackpad, + }, + ), + child: _buildScrollableTable(context), + ), + ), + _buildPagination(context), + ], + ); + } + + static const Map _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 get _tableColumnWidths { + return { + for (var i = 0; i < excelHeader.length; i++) + i: FixedColumnWidth(_staticColumnWidth(excelHeader[i])), + }; + } + + double get _totalTableWidth { + return excelHeader.fold( + 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, + ), ), - child: _buildScrollableTable(context), + ); + } + + Widget _tableDataCell(Map 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,259 +668,50 @@ class _activePolicyExcelErrorState extends State 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), - 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, - ), - ), + ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Table( + columnWidths: _tableColumnWidths, + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + children: [ + TableRow( + decoration: const BoxDecoration( + color: Color(0xFFD7E9EB), ), - ); - }).toList(), + children: excelHeader + .map(_tableHeaderCell) + .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, - ), - ), + Table( + columnWidths: _tableColumnWidths, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + border: const TableBorder( + horizontalInside: 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), + ), + children: _paginatedExcelData.map((row) { + return TableRow( + children: List.generate(excelHeader.length, (index) { + final cell = index < row.length + ? row[index] + : {'value': '-'}; + return _tableDataCell(cell); + }), + ); + }).toList(), ), ], ), @@ -736,67 +722,251 @@ class _activePolicyExcelErrorState extends State ); } - Widget _buildPagination(BuildContext context) { - final totalPages = (filteredExcelData.length / _rowsPerPage).ceil(); + 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, + ), + onPressed: () { + setState(() { + _currentPage = page; + }); + }, + child: Text(page.toString()), + ), + ); + } - if (totalPages <= 1) { - return const SizedBox.shrink(); // 👈 hide if only one page + Widget _buildPagination(BuildContext context) { + final totalItems = filteredExcelData.length; + final int startEntry = + totalItems == 0 ? 0 : ((_currentPage - 1) * _rowsPerPage) + 1; + int endEntry = _currentPage * _rowsPerPage; + if (endEntry > totalItems) endEntry = totalItems; + + final totalPages = + totalItems == 0 ? 1 : (filteredExcelData.length / _rowsPerPage).ceil(); + const visiblePageCount = 6; + + List getVisiblePages() { + if (totalPages <= visiblePageCount) { + return List.generate(totalPages, (i) => i + 1); + } + + 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, + ]; } - return Row( - mainAxisAlignment: MainAxisAlignment.end, // 👈 right aligned - children: [ - DropdownButton( - value: _rowsPerPage, - items: [5, 10, 15, 20, 50].map((int value) { - return DropdownMenuItem( - 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()), + 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, ), ), - IconButton( - icon: const Icon(Icons.chevron_right), - onPressed: _currentPage < totalPages - ? () => setState(() => _currentPage++) - : null, - ), - ], + Row( + children: [ + DropdownButton( + value: _rowsPerPage, + items: [6, 10, 15, 20, 50].map((int value) { + return DropdownMenuItem( + value: value, + child: Text( + ' $value ', + style: GoogleFonts.poppins(fontSize: 15), + ), + ); + }).toList(), + onChanged: totalItems == 0 + ? null + : (newValue) { + setState(() { + _rowsPerPage = newValue!; + _currentPage = 1; + }); + }, + ), + IconButton( + onPressed: _currentPage > 1 + ? () => setState(() => _currentPage--) + : null, + icon: const Icon(Icons.chevron_left), + ), + if (totalPages > 0 && !visiblePages.contains(1)) + Row( + children: [ + _buildPageButton(1), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Text('...'), + ), + ], + ), + 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( + onPressed: _currentPage < totalPages + ? () => setState(() => _currentPage++) + : null, + icon: const Icon(Icons.chevron_right), + ), + ], + ), + ], + ), ); } } + +bool hasExcelErrorTableData(Map 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 showExcelErrorMessageDialog( + BuildContext context, + String message, { + bool popRouteOnClose = false, +}) { + return showDialog( + 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 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( + 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( + context, + MaterialPageRoute( + 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.', + ); + } + } +} diff --git a/lib/presentation/hrPolicyDetails.dart b/lib/presentation/hrPolicyDetails.dart index 9af0cd7..283c716 100755 --- a/lib/presentation/hrPolicyDetails.dart +++ b/lib/presentation/hrPolicyDetails.dart @@ -566,7 +566,7 @@ class _HrPolicyDetailsState extends State 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 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 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 notLoggedIn++; } } - if (status == 'draft') draft++; + if (status == 'draft' && _isSelfRow(item)) draft++; } return { diff --git a/lib/presentation/postFileUpload.dart b/lib/presentation/postFileUpload.dart index 54d9eb5..db0a3ef 100755 --- a/lib/presentation/postFileUpload.dart +++ b/lib/presentation/postFileUpload.dart @@ -900,125 +900,138 @@ class _postFileUploadState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - /// Select File Action - Expanded( - flex: 5, - child: buildStyledDropdown( - label: 'Select File Action', - value: selectedKey, - items: getFileUploadMasterList, - onChanged: (val) { - setState(() { - selectedKey = 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( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - /// ✅ LABEL - RichText( - text: TextSpan( - text: 'Upload File', - style: GoogleFonts.poppins( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Colors.black, + 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, + ); + selectedValue = selectedItem['value']; + currentApiValue = selectedItem['key']; + showSampleButton = true; + }); + }, + ); + final uploadField = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + text: 'Upload File', + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + children: const [ + TextSpan( + text: '(Supported Formats: XLSX)', + style: TextStyle( + fontSize: 11, + color: Colors.grey, + fontWeight: FontWeight.w400, + ), ), - children: const [ - TextSpan( - text: '(Supported Formats: XLSX)', - style: TextStyle( - fontSize: 11, - color: Colors.grey, - fontWeight: FontWeight.w400, + ], + ), + ), + const SizedBox(height: 6), + DragTarget( + onAccept: (html.File droppedFile) { + setState(() { + fileName = droppedFile.name; + }); + _dragAndDropFile(droppedFile); + }, + builder: + (context, candidateData, rejectedData) { + return GestureDetector( + onTap: () { + if (selectedValue != null) { + _uploadFile(); + } else { + ToastHelper.showErrorToast( + context, + 'Please select file action', + ); + } + }, + child: Container( + height: 40, + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xFF00A6A6), + width: 1, ), ), - ], - ), - ), - - const SizedBox(height: 6), - - /// ✅ DOTTED UPLOAD BOX - DragTarget( - onAccept: (html.File droppedFile) { - setState(() { - fileName = droppedFile.name; - }); - _dragAndDropFile(droppedFile); - }, - builder: - (context, candidateData, rejectedData) { - return GestureDetector( - onTap: () { - if (selectedValue != null) { - _uploadFile(); - } else { - ToastHelper.showErrorToast( - context, - 'Please select file action', - ); - } - }, - child: Container( - height: 40, - padding: const EdgeInsets.symmetric( - horizontal: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: - BorderRadius.circular(8), - border: Border.all( - color: const Color(0xFF00A6A6), - width: 1, + child: Row( + children: [ + Expanded( + child: Text( + fileName ?? + 'Upload Your Documents', + overflow: TextOverflow.ellipsis, + style: GoogleFonts.poppins( + fontSize: 13, + color: fileName == null + ? Colors.grey + : Colors.black, + ), ), ), - child: Row( - children: [ - Expanded( - child: Text( - fileName ?? - 'Upload Your Documents', - overflow: - TextOverflow.ellipsis, - style: GoogleFonts.poppins( - fontSize: 13, - color: fileName == null - ? Colors.grey - : Colors.black, - ), - ), - ), - const Icon( - Icons.file_upload_outlined, - size: 18, - color: Colors.black, - ), - ], + const Icon( + Icons.file_upload_outlined, + size: 18, + color: Colors.black, ), - )); - }, - ), + ], + ), + ), + ); + }, + ), + ], + ); + + 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,27 +1159,35 @@ class _postFileUploadState extends State { ); } - return GridView.builder( - shrinkWrap: true, // ✅ IMPORTANT - physics: const NeverScrollableScrollPhysics(), // ✅ Disable inner scroll - padding: const EdgeInsets.all(16), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - childAspectRatio: 10, - ), - itemCount: _paginatedData.length, - itemBuilder: (context, index) { - final item = _paginatedData[index]; - return _buildFileCard(item); + return LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + final crossAxisCount = width < 640 ? 1 : 2; + const mainAxisExtent = 88.0; + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + mainAxisExtent: mainAxisExtent, + ), + itemCount: _paginatedData.length, + itemBuilder: (context, index) { + final item = _paginatedData[index]; + return _buildFileCard(item); + }, + ); }, ); } Widget _buildFileCard(Map 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 { 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 { 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 { 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,106 +1249,82 @@ class _postFileUploadState extends State { ), ], ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), ), - - /// 👉 RIGHT SIDE (ICON + STATUS + DOWNLOAD) - Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, + const SizedBox(width: 8), + Row( + mainAxisSize: MainAxisSize.min, children: [ - /// 🔴 Error + Status - Row( - children: [], - ), + if (item['file_error_status'] == '1') + Tooltip( + message: 'Info', + child: InkWell( + onTap: () async { + final String? token = + await tokenService.getCurrentToken(); + final String? empClientId = + await tokenService.readValue('empClientId'); + final String? empBranchId = + await tokenService.readValue('empClientBranchId'); - const SizedBox(height: 8), + if (token == null || + empClientId == null || + empBranchId == null) { + debugPrint( + '❌ Missing required data for navigation $token'); + return; + } - Row( - children: [ - if (item['file_error_status'] == '1') - InkWell( - onTap: () async { - logDebug(item); - // return; - final String? token = - await tokenService.getCurrentToken(); - final String? empClientId = - await tokenService.readValue('empClientId'); - 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}'); - return; - } - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => excelErrorScreen( - ClientId: empClientId, - policy_no: item['policy_no'], - action: item['file_action'], - created_at: item['created_at'], - clientBranchId: empBranchId, - Token: token, - TokenType: 'post', - id: item['id']), - ), - ); - }, - child: Icon( - Icons.error, - size: 16, - color: Colors.red, - ), - ), - SizedBox(width: 10), - _buildStatusChip(item['status']), - SizedBox(width: 10), - InkWell( - onTap: () { - getHrFileDownload(item['id'], item['file_name']); + 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, + ); }, - child: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - color: Color(0xFFC5F2F4), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFF76CED2)), - ), - child: Icon( - Icons.file_download_outlined, - color: Color(0xFF1D1B20), - size: 22, - ), + child: const Icon( + Icons.error, + size: 16, + color: Colors.red, ), ), - ], + ), + const SizedBox(width: 8), + _buildStatusChip(item['status']), + const SizedBox(width: 8), + Tooltip( + message: 'Download', + child: InkWell( + onTap: () { + getHrFileDownload(item['id'], item['file_name']); + }, + child: Container( + height: 28, + width: 28, + decoration: BoxDecoration( + color: Color(0xFFC5F2F4), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFF76CED2)), + ), + child: const Icon( + Icons.file_download_outlined, + color: Color(0xFF1D1B20), + size: 20, + ), + ), + ), ), - - /// ⬇ Download ], ), ], @@ -1428,75 +1419,109 @@ class _postFileUploadState extends State { List visiblePages = getVisiblePages(); - return Row( - mainAxisAlignment: MainAxisAlignment.end, + Widget paginationControls = Row( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Row( + DropdownButton( + value: _rowsPerPage, + items: [6, 10, 15, 20, 50].map((int value) { + return DropdownMenuItem( + value: value, + child: Text( + ' $value ', + style: GoogleFonts.poppins(fontSize: 15), + ), + ); + }).toList(), + onChanged: (newValue) { + setState(() { + _rowsPerPage = newValue!; + _currentPage = 1; + }); + }, + ), + IconButton( + tooltip: 'Previous Page', + onPressed: + _currentPage > 1 ? () => setState(() => _currentPage--) : null, + icon: const Icon(Icons.chevron_left), + ), + if (!visiblePages.contains(1)) + Row( children: [ - // Dropdown for rows per page - DropdownButton( - value: _rowsPerPage, - items: [6, 10, 15, 20, 50].map((int value) { - return DropdownMenuItem( - value: value, - child: Text(' $value ', - style: GoogleFonts.poppins(fontSize: 15)), - ); - }).toList(), - onChanged: (newValue) { - setState(() { - _rowsPerPage = newValue!; - _currentPage = 1; - }); - }, - ), - - // Previous button - IconButton( - tooltip: 'Previous Page', - onPressed: _currentPage > 1 - ? () => setState(() => _currentPage--) - : null, - icon: const Icon(Icons.chevron_left), - ), - - // First page + left ellipsis - if (!visiblePages.contains(1)) - Row(children: [ - _buildPageButton(1), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 4), - child: Text("..."), - ), - ]), - - // Visible page buttons - for (int page in visiblePages) _buildPageButton(page), - - // Right ellipsis + last page - if (!visiblePages.contains(totalPages)) - Row(children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 4), - child: Text("..."), - ), - _buildPageButton(totalPages), - ]), - - // Next button - IconButton( - onPressed: _currentPage < totalPages - ? () => setState(() => _currentPage++) - : null, - icon: const Icon(Icons.chevron_right), + _buildPageButton(1), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Text('...'), ), ], ), + for (int page in visiblePages) _buildPageButton(page), + if (!visiblePages.contains(totalPages) && totalPages > 0) + Row( + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Text('...'), + ), + _buildPageButton(totalPages), + ], + ), + IconButton( + onPressed: _currentPage < totalPages + ? () => setState(() => _currentPage++) + : null, + 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) { diff --git a/lib/presentation/preFileUpload.dart b/lib/presentation/preFileUpload.dart index 97c0bbc..41aec7e 100755 --- a/lib/presentation/preFileUpload.dart +++ b/lib/presentation/preFileUpload.dart @@ -930,66 +930,140 @@ class _excelVerifyState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - SizedBox( - width: 260, // 👈 set your required width - child: _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); + 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 open date changed, clear close date - if (openDateController.text != formatted) { - closeDateController.clear(); - } + if (openDateController.text != + formatted) { + closeDateController.clear(); + } - openDateController.text = formatted; - } - }, - ), - ), - const SizedBox(width: 16), - SizedBox( - width: 260, // 👈 same width - child: _dateField( - label: 'Enrolment Close Date', - controller: closeDateController, - onTap: () async { - if (openDateController.text.isEmpty) { - ToastHelper.showErrorToast(context, - 'Please select Enrolment Open Date first'); - return; - } + 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 openDate = + DateFormat('dd-MM-yyyy').parse( + openDateController.text, + ); - final picked = await showDatePicker( - context: context, - firstDate: - openDate, // ✅ Cannot select before open date - lastDate: DateTime(2100), - initialDate: openDate, - ); + final picked = await showDatePicker( + context: context, + firstDate: openDate, + lastDate: DateTime(2100), + initialDate: openDate, + ); - if (picked != null) { - closeDateController.text = - DateFormat('dd-MM-yyyy').format(picked); - } - }, - ), - ), - ], + if (picked != null) { + closeDateController.text = + DateFormat('dd-MM-yyyy') + .format(picked); + } + }, + ), + ], + ); + } + + return Row( + children: [ + SizedBox( + width: 260, + child: _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(width: 16), + SizedBox( + 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', + ); + 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); + } + }, + ), + ), + ], + ); + }, ), SizedBox(height: 20), Row( @@ -1288,20 +1362,28 @@ class _excelVerifyState extends State { return const Center(child: Text('No uploaded files')); } - return GridView.builder( - shrinkWrap: true, // ✅ IMPORTANT - physics: const NeverScrollableScrollPhysics(), // ✅ Disable inner scroll - padding: const EdgeInsets.all(16), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - childAspectRatio: 10, - ), - itemCount: _paginatedData.length, - itemBuilder: (context, index) { - final item = _paginatedData[index]; - return _buildFileCard(item); + return LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + final crossAxisCount = width < 640 ? 1 : 2; + const mainAxisExtent = 88.0; + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + mainAxisExtent: mainAxisExtent, + ), + itemCount: _paginatedData.length, + itemBuilder: (context, index) { + final item = _paginatedData[index]; + return _buildFileCard(item); + }, + ); }, ); } @@ -1333,7 +1415,7 @@ class _excelVerifyState extends State { Widget _buildFileCard(Map 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 { 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 { 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 { 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,113 +1477,83 @@ class _excelVerifyState extends State { ), ], ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), ), - - /// 👉 RIGHT SIDE (ICON + STATUS + DOWNLOAD) - Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, + const SizedBox(width: 8), + Row( + mainAxisSize: MainAxisSize.min, children: [ - /// 🔴 Error + Status - Row( - children: [], - ), + if (item['file_error_status'] == '1') + Tooltip( + message: 'Info', + child: InkWell( + onTap: () async { + final String? token = + await tokenService.getCurrentToken(); + final String? enrollmentClient_id = await tokenService + .readValue('enrollmentClient_id'); + final String? enrollmentEmpClientBranchId = + await tokenService + .readValue('enrollmentEmpClientBranchId'); - const SizedBox(height: 8), + if (token == null || + enrollmentClient_id == null || + enrollmentEmpClientBranchId == null) { + debugPrint( + '❌ Missing required data for navigation $token'); + return; + } - Row( - children: [ - if (item['file_error_status'] == '1') - Tooltip( - message: 'Info', // Added tooltip name - child: InkWell( - onTap: () async { - logDebug(item); - // return; - final String? token = - await tokenService.getCurrentToken(); - final String? enrollmentClient_id = await tokenService - .readValue('enrollmentClient_id'); - final String? enrollmentEmpClientBranchId = - 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}'); - 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'], - clientBranchId: enrollmentEmpClientBranchId, - Token: token, - TokenType: 'pre', - id: item['id']), - ), - ); - }, - child: Icon( - Icons.error, - size: 16, - color: Colors.red, - ), - ), - ), - SizedBox(width: 10), - _buildStatusChip(item['status']), - SizedBox(width: 10), - Tooltip( - message: 'Download', // Added tooltip name - child: InkWell( - onTap: () { - getHrFileDownload(item['id'], item['file_name']); - }, - child: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - color: Color(0xFFC5F2F4), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFF76CED2)), - ), - child: Icon( - Icons.file_download_outlined, - color: Color(0xFF1D1B20), - size: 22, - ), - ), + 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, + ); + }, + child: const Icon( + Icons.error, + size: 16, + color: Colors.red, ), ), - ], + ), + const SizedBox(width: 8), + _buildStatusChip(item['status']), + const SizedBox(width: 8), + Tooltip( + message: 'Download', + child: InkWell( + onTap: () { + getHrFileDownload(item['id'], item['file_name']); + }, + child: Container( + height: 28, + width: 28, + decoration: BoxDecoration( + color: Color(0xFFC5F2F4), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFF76CED2)), + ), + child: const Icon( + Icons.file_download_outlined, + color: Color(0xFF1D1B20), + size: 20, + ), + ), + ), ), - - /// ⬇ Download ], ), ], @@ -1586,86 +1632,106 @@ class _excelVerifyState extends State { List 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 - 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 --- + Widget paginationControls = Row( + mainAxisSize: MainAxisSize.min, + children: [ + DropdownButton( + value: _rowsPerPage, + items: [6, 10, 15, 20, 50].map((int value) { + return DropdownMenuItem( + value: value, + child: Text( + ' $value ', + style: GoogleFonts.poppins(fontSize: 15), + ), + ); + }).toList(), + onChanged: (newValue) { + setState(() { + _rowsPerPage = newValue!; + _currentPage = 1; + }); + }, + ), + IconButton( + onPressed: + _currentPage > 1 ? () => setState(() => _currentPage--) : null, + icon: const Icon(Icons.chevron_left), + ), + if (!visiblePages.contains(1)) Row( children: [ - // Dropdown for rows per page - DropdownButton( - value: _rowsPerPage, - // focusColor: Colors.transparent, // Fix: Removes the grey/blue highlight on change - items: [6, 10, 15, 20, 50].map((int value) { - return DropdownMenuItem( - value: value, - child: Text(' $value ', - style: GoogleFonts.poppins(fontSize: 15)), - ); - }).toList(), - onChanged: (newValue) { - setState(() { - _rowsPerPage = newValue!; - _currentPage = 1; - }); - }, - ), - - // Previous button - IconButton( - onPressed: _currentPage > 1 - ? () => setState(() => _currentPage--) - : null, - icon: const Icon(Icons.chevron_left), - ), - - // First page + left ellipsis - if (!visiblePages.contains(1)) - Row(children: [ - _buildPageButton(1), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 4), - child: Text("..."), - ), - ]), - - // Visible page buttons - for (int page in visiblePages) _buildPageButton(page), - - // Right ellipsis + last page - if (!visiblePages.contains(totalPages) && totalPages > 0) - Row(children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 4), - child: Text("..."), - ), - _buildPageButton(totalPages), - ]), - - // Next button - IconButton( - onPressed: _currentPage < totalPages - ? () => setState(() => _currentPage++) - : null, - icon: const Icon(Icons.chevron_right), + _buildPageButton(1), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Text('...'), ), ], ), - ], + for (int page in visiblePages) _buildPageButton(page), + if (!visiblePages.contains(totalPages) && totalPages > 0) + Row( + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Text('...'), + ), + _buildPageButton(totalPages), + ], + ), + IconButton( + onPressed: _currentPage < totalPages + ? () => setState(() => _currentPage++) + : null, + 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, + ), + ), + ], + ); + }, ), ); } diff --git a/lib/service/token_storage_service.dart b/lib/service/token_storage_service.dart index 33203fc..d6b6fe3 100755 --- a/lib/service/token_storage_service.dart +++ b/lib/service/token_storage_service.dart @@ -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