import 'dart:convert'; 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 'package:universal_html/html.dart' as html; import '../../customAppBar/base_layout.dart'; import '../../customAppBar/toastHelper.dart'; import '../../logger.dart'; 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'; import 'claims_overview_scope.dart'; import 'claims_overview_tabs.dart'; import 'claims_overview_theme.dart'; class ClaimsOverviewDashboard extends StatefulWidget { const ClaimsOverviewDashboard({super.key}); @override State createState() => _ClaimsOverviewDashboardState(); } class _ClaimsOverviewDashboardState extends State with TickerProviderStateMixin { late TabController _tabController; late AnimationController _headerFadeController; late Animation _headerFade; late final ScrollController _scrollController; late final List _sectionKeys; late final ApiService _apiService; final _tokenService = TokenStorageService(); bool _isLoading = true; bool _isRefreshing = false; bool _isExportingPdf = false; bool _isExportingExcel = false; bool _sessionChecked = false; int _replayToken = 0; int _loadGeneration = 0; int _currentTab = 0; ClaimsOverviewViewData _viewData = ClaimsOverviewViewData.empty(); EnrollmentOverviewViewData _enrollmentViewData = EnrollmentOverviewViewData.empty(); String? _clientId; String? _clientBranchId; String? _selectedPolicyId; List> _activePolicies = []; Uint8List? _clientLogoBytes; /// Raw `generated_at` from claims API (e.g. `01-07-2026`). String? _apiGeneratedAt; String? get _formattedGeneratedAt { final raw = _apiGeneratedAt?.trim(); if (raw == null || raw.isEmpty) return null; return _formatApiGeneratedAt(raw); } /// Formats API `generated_at` for display. Returns null when empty. static String? _formatApiGeneratedAt(String raw) { final value = raw.trim(); if (value.isEmpty) return null; for (final pattern in ['dd-MM-yyyy', 'yyyy-MM-dd', 'dd/MM/yyyy']) { try { final parsed = DateFormat(pattern).parseStrict(value); return DateFormat('d MMM yyyy').format(parsed); } catch (_) {} } final iso = DateTime.tryParse(value); if (iso != null) return DateFormat('d MMM yyyy').format(iso); return value; } static String? _parseApiGeneratedAt(Map response) { final raw = response['generated_at']?.toString().trim(); if (raw == null || raw.isEmpty) return null; return raw; } static const _tabs = [ (Icons.dashboard_outlined, 'Overview'), (Icons.timeline, 'Claims Overview'), (Icons.analytics_outlined, 'Claims Analysis'), (Icons.people_outline, 'Demographics'), (Icons.location_city_outlined, 'Hospitals & Geography'), (Icons.medical_services_outlined, 'Specialty Analysis'), (Icons.group_add_outlined, 'Insured'), ]; @override void initState() { super.initState(); _apiService = ApiService(context); _scrollController = ScrollController(); _sectionKeys = List.generate(_tabs.length, (_) => GlobalKey()); _tabController = TabController(length: _tabs.length, vsync: this); _tabController.addListener(_onTabChanged); _headerFadeController = AnimationController( vsync: this, duration: const Duration(milliseconds: 600), ); _headerFade = CurvedAnimation( parent: _headerFadeController, curve: Curves.easeOut, ); _headerFadeController.forward(); _checkToken(); } Future _checkToken() async { final token = await _tokenService.getCurrentToken(); if (token == null || token.trim().isEmpty) { if (!mounted) return; ToastHelper.showErrorToast(context, 'Session Out'); Navigator.pushNamedAndRemoveUntil( context, 'hrLogin', (route) => false, ); return; } if (!mounted) return; setState(() => _sessionChecked = true); _logHrActivity('opened_hr_dashboard'); await _loadDashboard(); } Future _logHrActivity(String activity) async { try { final token = await _tokenService.getCurrentToken(); if (token == null || token.isEmpty) return; var postId = await _tokenService.readValue('empHrId'); var preId = await _tokenService.readValue('enrollmentEmpPrimaryId'); if (postId == null || postId.isEmpty || preId == null || preId.isEmpty) { final decoded = _tokenService.getDecodedToken(); postId ??= decoded?['post_hr_id']?.toString(); preId ??= decoded?['pre_hr_id']?.toString(); } if (postId == null || postId.isEmpty || preId == null || preId.isEmpty) { return; } await _apiService.getPostLogHrActivity(postId, preId, token, activity); } catch (e) { logDebug('logHrActivity ($activity) failed: $e'); } } void _onTabChanged() { if (_tabController.indexIsChanging) return; final index = _tabController.index; if (_currentTab != index) { setState(() => _currentTab = index); } } void _scrollToSection(int index) { setState(() => _currentTab = index); WidgetsBinding.instance.addPostFrameCallback((_) { final sectionContext = _sectionKeys[index].currentContext; if (sectionContext == null) return; Scrollable.ensureVisible( sectionContext, duration: const Duration(milliseconds: 450), curve: Curves.easeInOut, alignment: 0.02, ); }); } bool _isStaleLoad(int loadId, [String? policyId]) { if (!mounted || loadId != _loadGeneration) return true; if (policyId != null && policyId != _selectedPolicyId) return true; return false; } Future _loadDashboard({ bool reloadPolicies = false, bool forceRefresh = false, }) async { final loadId = ++_loadGeneration; final isInitialLoad = _replayToken == 0; setState(() { if (isInitialLoad) { _isLoading = true; } else { _isRefreshing = true; } }); try { _clientId = await _tokenService.readValue('empClientId'); _clientBranchId = await _tokenService.readValue('empClientBranchId'); final hrId = await _tokenService.readValue('empHrId'); final token = await _tokenService.getCurrentToken(); if (_isStaleLoad(loadId)) return; if (token != null && token.isNotEmpty) { await _apiService.getTokenLoadAPI(token); } if (_isStaleLoad(loadId)) return; if (isInitialLoad || reloadPolicies || _activePolicies.isEmpty) { await _loadPolicyList(token ?? '', hrId ?? ''); // Keep an existing user selection; only default to first when none set. if (_isStaleLoad(loadId)) return; setState(() {}); } if (_selectedPolicyId == null) { if (_isStaleLoad(loadId)) return; setState(() { _viewData = ClaimsOverviewViewData.empty( error: 'No active policy found', ); _enrollmentViewData = EnrollmentOverviewViewData.empty(); _apiGeneratedAt = null; _isLoading = false; _isRefreshing = false; }); return; } final policyId = _selectedPolicyId!; final branchId = _clientBranchId ?? ''; if (forceRefresh) { await ClaimsOverviewCache.clear(branchId, policyId); } if (_isStaleLoad(loadId, policyId)) return; if (!forceRefresh) { final cached = await ClaimsOverviewCache.read(branchId, policyId); if (cached != null) { if (_isStaleLoad(loadId, policyId)) return; _applyCacheEntry(cached); setState(() { _isLoading = false; _isRefreshing = false; if (_replayToken == 0) { _replayToken++; } }); return; } } final response = await _apiService.getClaimsCollectionV2All( clientPolicyId: policyId, ); if (_isStaleLoad(loadId, policyId)) return; final enrollmentResponse = await _apiService.getEnrollmentCollectionV1All( clientPolicyId: policyId, ); if (_isStaleLoad(loadId, policyId)) return; final apiGeneratedAt = _parseApiGeneratedAt(response); final claimsOk = ClaimsKpiParser.isSuccessResponse(response); var claimsData = await ClaimsOverviewViewData.enrichFromApiResponse( response, fetchKpi: (slug) => _apiService.getClaimsCollectionV2Kpi( slug, clientPolicyId: policyId, ), ); if (_isStaleLoad(loadId, policyId)) return; var enrollmentData = _parseEnrollmentResponse(enrollmentResponse); final selectedPolicy = _selectedPolicy(); if (selectedPolicy != null) { claimsData = claimsData.withPolicyFallback(selectedPolicy); enrollmentData = enrollmentData.withPolicyFallback(selectedPolicy); } if (!claimsOk) { final message = response['message']?.toString() ?? 'Failed to load claims data'; claimsData = ClaimsOverviewViewData( kpiBySlug: claimsData.kpiBySlug, loadError: message, ); if (claimsData.kpiBySlug.isEmpty) { if (_isStaleLoad(loadId, policyId)) return; setState(() { _viewData = claimsData; _enrollmentViewData = enrollmentData; _apiGeneratedAt = apiGeneratedAt; _isLoading = false; _isRefreshing = false; _replayToken++; }); ToastHelper.showErrorToast(context, message); return; } } await ClaimsOverviewCache.write( branchId, ClaimsOverviewCacheEntry( policyId: policyId, claimsKpiBySlug: claimsData.kpiBySlug, enrollmentKpiBySlug: enrollmentData.kpiBySlug, generatedAt: apiGeneratedAt, claimsLoadError: claimsData.loadError, enrollmentLoadError: enrollmentData.loadError, ), ); if (_isStaleLoad(loadId, policyId)) return; setState(() { _viewData = claimsData; _enrollmentViewData = enrollmentData; _apiGeneratedAt = apiGeneratedAt; _isLoading = false; _isRefreshing = false; _replayToken++; }); } catch (e) { if (_isStaleLoad(loadId)) return; setState(() { _viewData = ClaimsOverviewViewData.empty(error: e.toString()); _enrollmentViewData = EnrollmentOverviewViewData.empty(error: e.toString()); _apiGeneratedAt = null; _isLoading = false; _isRefreshing = false; }); ToastHelper.showErrorToast(context, 'Failed to load claims overview'); } } void _applyCacheEntry(ClaimsOverviewCacheEntry entry) { _viewData = ClaimsOverviewViewData( kpiBySlug: Map.from(entry.claimsKpiBySlug), loadError: entry.claimsLoadError, ); _enrollmentViewData = EnrollmentOverviewViewData( kpiBySlug: Map.from(entry.enrollmentKpiBySlug), loadError: entry.enrollmentLoadError, ); _apiGeneratedAt = entry.generatedAt; } Future _loadPolicyList(String token, String hrId) async { _clientId = await _tokenService.readValue('empClientId'); _clientBranchId = await _tokenService.readValue('empClientBranchId'); if (_clientId == null || _clientBranchId == null) return; final policyRes = await _apiService.getActiveCashDepositDetailsToApi( _clientId!, _clientBranchId!, hrId, token, 1, ); if (policyRes['status'] != 'success' || policyRes['data'] is! List) { _activePolicies = []; _selectedPolicyId = null; return; } _activePolicies = List>.from(policyRes['data']) ..sort((a, b) { final aLabel = '${a['type']} - ${a['policy_no']}'; final bLabel = '${b['type']} - ${b['policy_no']}'; return aLabel.compareTo(bLabel); }); if (_activePolicies.isNotEmpty) { final firstId = _activePolicies.first['client_policy_id'].toString(); final stillValid = _selectedPolicyId != null && _activePolicies.any( (p) => p['client_policy_id'].toString() == _selectedPolicyId, ); if (!stillValid) { _selectedPolicyId = firstId; } await _loadClientLogoForSelectedPolicy(); } else { _selectedPolicyId = null; _clientLogoBytes = null; } } EnrollmentOverviewViewData _parseEnrollmentResponse( Map response, ) { if (!ClaimsKpiParser.isSuccessResponse(response)) { return EnrollmentOverviewViewData.empty( error: response['message']?.toString() ?? 'Failed to load enrollment data', ); } return EnrollmentOverviewViewData.fromApiResponse(response); } Map? _selectedPolicy() { if (_selectedPolicyId == null) return null; for (final policy in _activePolicies) { if (policy['client_policy_id'].toString() == _selectedPolicyId) { return policy; } } return null; } Future _loadClientLogoForSelectedPolicy() async { final url = _selectedPolicy()?['client_logo']?.toString().trim(); if (url == null || url.isEmpty) { if (mounted) setState(() => _clientLogoBytes = null); return; } try { final response = await http.get(Uri.parse(url)); if (!mounted) return; if (response.statusCode == 200 && response.bodyBytes.isNotEmpty) { setState(() => _clientLogoBytes = response.bodyBytes); } else { setState(() => _clientLogoBytes = null); } } catch (_) { if (mounted) setState(() => _clientLogoBytes = null); } } void _onPolicySelected(String policyId) { if (policyId == _selectedPolicyId) return; setState(() => _selectedPolicyId = policyId); _loadClientLogoForSelectedPolicy(); _loadDashboard(); } String _selectedPolicyLabel() { if (_selectedPolicyId == null) return 'Select Policy'; for (final policy in _activePolicies) { if (policy['client_policy_id'].toString() == _selectedPolicyId) { return '${policy['type']} - ${policy['policy_no']}'; } } return 'Policy $_selectedPolicyId'; } Future _downloadChartsPdf() async { if (_isExportingPdf || _isExportingExcel || _isLoading) return; setState(() => _isExportingPdf = true); if (!mounted) return; final navigator = Navigator.of(context, rootNavigator: true); final overlay = navigator.overlay; if (overlay == null) { setState(() => _isExportingPdf = false); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Could not generate PDF. Overlay unavailable.', style: GoogleFonts.poppins(), ), backgroundColor: Colors.red.shade700, behavior: SnackBarBehavior.floating, ), ); return; } var progressLabel = 'Preparing PDF…'; void Function(void Function())? updateProgressDialog; showDialog( context: context, barrierDismissible: false, useRootNavigator: true, builder: (ctx) => PopScope( canPop: false, child: StatefulBuilder( builder: (context, setDialogState) { updateProgressDialog = setDialogState; return AlertDialog( content: Row( children: [ const CircularProgressIndicator(), const SizedBox(width: 20), Expanded( child: Text( 'Generating PDF…\n$progressLabel', style: GoogleFonts.poppins(fontSize: 14), ), ), ], ), ); }, ), ), ); try { if (_clientLogoBytes == null) { await _loadClientLogoForSelectedPolicy(); } await exportClaimsOverviewChartsPdf( overlay: overlay, replayToken: _replayToken, viewData: _viewData, enrollmentViewData: _enrollmentViewData, policyId: _selectedPolicyId ?? '', dashboardInfo: ClaimsPdfDashboardInfo( policyLabel: _selectedPolicyLabel(), clientName: _tokenService.getSelectedBranch()?['client_name']?.toString(), branchName: _tokenService.getSelectedBranch()?['branch_name']?.toString(), misCreationDate: _formattedGeneratedAt, ), clientLogoBytes: _clientLogoBytes, onProgress: (_, __, label) { updateProgressDialog?.call(() { progressLabel = label; }); }, ); await _logHrActivity('export_hr_dashboard_data_pdf'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Claims Overview PDF downloaded', style: GoogleFonts.poppins(), ), behavior: SnackBarBehavior.floating, ), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Could not generate PDF. Please try again. ($e)', style: GoogleFonts.poppins(), ), backgroundColor: Colors.red.shade700, behavior: SnackBarBehavior.floating, ), ); } finally { if (mounted && navigator.canPop()) { navigator.pop(); } if (mounted) setState(() => _isExportingPdf = false); } } Future _downloadExcel() async { if (_isExportingExcel || _isExportingPdf || _isLoading) return; final policyId = _selectedPolicyId; if (policyId == null || policyId.isEmpty) { if (!mounted) return; ToastHelper.showErrorToast(context, 'Select a policy to download Excel'); return; } setState(() => _isExportingExcel = true); try { final response = await _apiService.downloadClaimsCollectionReportExcel( clientPolicyId: policyId, ); final apiMessage = _excelDownloadErrorMessage(response); if (apiMessage != null) { if (!mounted) return; ToastHelper.showErrorToast(context, apiMessage); return; } if (response.statusCode != 200 || response.bodyBytes.isEmpty) { throw Exception('Download failed (${response.statusCode})'); } final apiContentType = response.headers['content-type'] ?? ''; final contentDisposition = response.headers['content-disposition'] ?? ''; final excelContentType = apiContentType.contains('spreadsheetml') || apiContentType.contains('ms-excel') ? apiContentType : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; final utf8FileNameMatch = RegExp( "filename\\*=UTF-8''([^;]+)", caseSensitive: false, ).firstMatch(contentDisposition); final plainFileNameMatch = RegExp( 'filename="?([^";]+)"?', caseSensitive: false, ).firstMatch(contentDisposition); final rawFileName = utf8FileNameMatch?.group(1) ?? plainFileNameMatch?.group(1); final fileName = rawFileName != null && rawFileName.trim().isNotEmpty ? Uri.decodeComponent(rawFileName.trim()) : 'claims_collection_report_$policyId.xlsx'; final blob = html.Blob([response.bodyBytes], excelContentType); final url = html.Url.createObjectUrlFromBlob(blob); final anchor = html.AnchorElement(href: url) ..setAttribute('download', fileName) ..click(); html.Url.revokeObjectUrl(url); await _logHrActivity('export_hr_dashboard_data_excel'); if (!mounted) return; ToastHelper.showSuccessToast( context, 'Claims Overview Excel downloaded', ); } catch (e) { if (!mounted) return; ToastHelper.showErrorToast( context, 'Could not download Excel. Please try again.', ); logDebug('Excel download failed: $e'); } finally { if (mounted) setState(() => _isExportingExcel = false); } } /// Returns API error message when the Excel endpoint responds with JSON /// `{ "status": false, "message": "..." }` instead of a file. String? _excelDownloadErrorMessage(http.Response response) { final contentType = response.headers['content-type'] ?? ''; final body = response.body.trim(); final looksLikeJson = contentType.contains('application/json') || body.startsWith('{') || body.startsWith('['); if (!looksLikeJson || body.isEmpty) return null; try { final decoded = jsonDecode(body); if (decoded is! Map) return null; final status = decoded['status']; final isFailure = status == false || status == 0 || status?.toString().toLowerCase() == 'false' || status?.toString().toLowerCase() == 'error'; if (!isFailure) return null; final message = decoded['message']?.toString().trim(); if (message != null && message.isNotEmpty) return message; return 'Could not download Excel.'; } catch (_) { return null; } } @override void dispose() { _tabController.removeListener(_onTabChanged); _tabController.dispose(); _scrollController.dispose(); _headerFadeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (!_sessionChecked) { return const Scaffold( body: Center( child: CircularProgressIndicator( color: Color(0xFF00999E), ), ), ); } return BaseLayout( child: SecurePopScope( child: Container( color: ClaimsOverviewTheme.pageBg, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ FadeTransition( opacity: _headerFade, child: _buildHeader(), ), ClaimsTopLoadingBar(visible: _isRefreshing), _buildTabBar(), Expanded( child: ClaimsOverviewScope( key: ValueKey('claims-$_selectedPolicyId-$_replayToken'), data: _viewData, child: EnrollmentOverviewScope( key: ValueKey('enrollment-$_selectedPolicyId-$_replayToken'), data: _enrollmentViewData, child: ClaimsTabAnimatedShell( isLoading: _isLoading, skeletonBlocks: 5, child: Scrollbar( controller: _scrollController, thumbVisibility: true, child: SingleChildScrollView( controller: _scrollController, physics: _isRefreshing ? const NeverScrollableScrollPhysics() : const ClampingScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < _tabs.length; i++) ...[ _buildSection( index: i, replayToken: _replayToken, ), if (i < _tabs.length - 1) const SizedBox(height: 8), ], const SizedBox(height: 24), ], ), ), ), ), ), ), ), // _buildFooter(), ], ), ), ), ); } Widget _buildHeader() { return Container( padding: const EdgeInsets.fromLTRB(20, 16, 20, 12), decoration: BoxDecoration( color: ClaimsOverviewTheme.cardBg, borderRadius: BorderRadius.circular(12), border: Border.all(color: ClaimsOverviewTheme.border), ), child: Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ TweenAnimationBuilder( tween: Tween(begin: 0.85, end: 1), duration: const Duration(milliseconds: 500), curve: Curves.elasticOut, builder: (_, scale, child) => Transform.scale(scale: scale, child: child), child: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: ClaimsOverviewTheme.primary.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(10), ), child: const Icon( Icons.shield_outlined, color: ClaimsOverviewTheme.primary, size: 28, ), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Claims Overview', style: GoogleFonts.poppins( fontSize: 22, fontWeight: FontWeight.w700, color: ClaimsOverviewTheme.textPrimary, ), ), const SizedBox(height: 4), Row( children: [ Text( 'Policy & Claims Analytics', style: GoogleFonts.poppins( fontSize: 13, color: ClaimsOverviewTheme.textSecondary, ), ), if (_formattedGeneratedAt != null) ...[ const SizedBox(width: 12), ClaimsLiveIndicator( createdAt: _formattedGeneratedAt, ), ], ClaimsRefreshIcon( isRefreshing: _isRefreshing, onPressed: () => _loadDashboard(forceRefresh: true), ), ], ), ], ), ), _buildPolicyBadge(), const SizedBox(width: 12), _headerActions(), ], ), ], ), ); } Widget _buildPolicyBadge() { if (_activePolicies.isEmpty) { return Material( color: ClaimsOverviewTheme.pageBg, borderRadius: BorderRadius.circular(24), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( border: Border.all(color: ClaimsOverviewTheme.border), borderRadius: BorderRadius.circular(24), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( _selectedPolicyLabel(), style: GoogleFonts.poppins( fontSize: 13, fontWeight: FontWeight.w600, ), ), ], ), ), ); } return Material( color: ClaimsOverviewTheme.pageBg, borderRadius: BorderRadius.circular(24), child: SizedBox( width: 320, height: 40, child: SearchAnchor( viewBackgroundColor: ClaimsOverviewTheme.cardBg, viewConstraints: const BoxConstraints(maxHeight: 260), builder: (context, controller) { return InkWell( onTap: controller.openView, borderRadius: BorderRadius.circular(24), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14), decoration: BoxDecoration( border: Border.all(color: ClaimsOverviewTheme.border), borderRadius: BorderRadius.circular(24), ), child: Row( children: [ Expanded( child: Text( _selectedPolicyLabel(), style: GoogleFonts.poppins( fontSize: 13, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ), ), const Icon( Icons.arrow_drop_down, color: ClaimsOverviewTheme.textSecondary, ), ], ), ), ); }, suggestionsBuilder: (context, controller) { final input = controller.text.toLowerCase(); return _activePolicies .where((policy) { final type = policy['type'].toString().toLowerCase(); final policyNo = policy['policy_no'].toString().toLowerCase(); return type.contains(input) || policyNo.contains(input); }) .map((policy) { final label = '${policy['type']} - ${policy['policy_no']}'; return ListTile( dense: true, title: Text( label, style: GoogleFonts.poppins(fontSize: 13), ), onTap: () { controller.closeView(label); _onPolicySelected( policy['client_policy_id'].toString(), ); }, ); }) .toList(); }, ), ), ); } Widget _headerActions() { final isBusy = _isRefreshing || _isExportingPdf || _isExportingExcel || _isLoading; final isExporting = _isExportingPdf || _isExportingExcel; return Padding( padding: const EdgeInsets.only(left: 4), child: PopupMenuButton( tooltip: 'Download', enabled: !isBusy, offset: const Offset(0, 36), onSelected: (value) { if (value == 'pdf') { _downloadChartsPdf(); } else if (value == 'excel') { _downloadExcel(); } }, itemBuilder: (context) => [ PopupMenuItem( value: 'pdf', child: Row( children: [ const Icon(Icons.picture_as_pdf_outlined, size: 18), const SizedBox(width: 8), Text('PDF', style: GoogleFonts.poppins(fontSize: 13)), ], ), ), PopupMenuItem( value: 'excel', child: Row( children: [ const Icon(Icons.table_chart_outlined, size: 18), const SizedBox(width: 8), Text('Excel', style: GoogleFonts.poppins(fontSize: 13)), ], ), ), ], icon: isExporting ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon( Icons.download_outlined, size: 20, color: ClaimsOverviewTheme.textSecondary, ), ), ); } Widget _buildTabBar() { return Container( margin: const EdgeInsets.only(top: 12), decoration: BoxDecoration( color: ClaimsOverviewTheme.cardBg, borderRadius: BorderRadius.circular(12), border: Border.all(color: ClaimsOverviewTheme.border), ), child: Column( children: [ if (_isRefreshing) Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: Align( alignment: Alignment.centerRight, child: Text( 'Updating…', style: GoogleFonts.poppins( fontSize: 11, color: ClaimsOverviewTheme.textSecondary, ), ), ), ), TabBar( controller: _tabController, onTap: _scrollToSection, isScrollable: true, tabAlignment: TabAlignment.start, labelColor: ClaimsOverviewTheme.primary, unselectedLabelColor: ClaimsOverviewTheme.textSecondary, indicatorColor: ClaimsOverviewTheme.primary, indicatorWeight: 3, indicatorSize: TabBarIndicatorSize.label, labelStyle: GoogleFonts.poppins( fontSize: 13, fontWeight: FontWeight.w600, ), unselectedLabelStyle: GoogleFonts.poppins(fontSize: 13), tabs: List.generate(_tabs.length, (i) { final t = _tabs[i]; final selected = _currentTab == i; return Tab( height: 48, child: AnimatedScale( scale: selected ? 1.02 : 1.0, duration: const Duration(milliseconds: 200), curve: Curves.easeOut, child: Row( mainAxisSize: MainAxisSize.min, children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 200), child: Icon( t.$1, key: ValueKey('$i-$selected'), size: 18, color: selected ? ClaimsOverviewTheme.primary : ClaimsOverviewTheme.textSecondary, ), ), const SizedBox(width: 6), Text(t.$2), ], ), ), ); }), ), ], ), ); } Widget _buildSection({ required int index, required int replayToken, }) { final tab = _tabs[index]; return Container( key: _sectionKeys[index], margin: const EdgeInsets.only(top: 16), decoration: BoxDecoration( color: ClaimsOverviewTheme.cardBg, borderRadius: BorderRadius.circular(12), border: Border.all(color: ClaimsOverviewTheme.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), child: Row( children: [ Icon( tab.$1, size: 22, color: ClaimsOverviewTheme.primary, ), const SizedBox(width: 10), Text( tab.$2, style: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.w700, color: ClaimsOverviewTheme.textPrimary, ), ), ], ), ), const SizedBox(height: 12), ClaimsOverviewTab( index: index, replayToken: replayToken, isActive: true, scrollable: false, ), const SizedBox(height: 16), ], ), ); } Widget _buildFooter() { return Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Center( child: RichText( text: TextSpan( style: GoogleFonts.poppins( fontSize: 12, color: ClaimsOverviewTheme.textSecondary, ), children: [ const TextSpan(text: 'Powered by '), TextSpan( text: 'Nhance', style: GoogleFonts.poppins( fontWeight: FontWeight.w700, color: ClaimsOverviewTheme.orange, ), ), const TextSpan(text: ' Insights · Light Theme'), ], ), ), ), ); } }