import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:http/http.dart' as http; import '../../customAppBar/base_layout.dart'; import '../../customAppBar/toastHelper.dart'; import '../../service/api_service.dart'; import '../../service/secure_pop_scope.dart'; import '../../service/token_storage_service.dart'; import 'claims_collection_kpi.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; int _replayToken = 0; int _currentTab = 0; ClaimsOverviewViewData _viewData = ClaimsOverviewViewData.empty(); EnrollmentOverviewViewData _enrollmentViewData = EnrollmentOverviewViewData.empty(); String? _clientId; String? _clientBranchId; String? _selectedPolicyId; List> _activePolicies = []; Uint8List? _clientLogoBytes; 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(); _loadDashboard(); } 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, ); }); } Future _loadDashboard({bool reloadPolicies = false}) async { 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 (token != null && token.isNotEmpty) { await _apiService.getTokenLoadAPI(token); } if (isInitialLoad || reloadPolicies || _activePolicies.isEmpty) { await _loadPolicyList(token ?? '', hrId ?? ''); if (isInitialLoad && _activePolicies.isNotEmpty) { _selectedPolicyId = _activePolicies.first['client_policy_id'].toString(); } if (!mounted) return; setState(() {}); } if (_selectedPolicyId == null) { if (!mounted) return; setState(() { _viewData = ClaimsOverviewViewData.empty( error: 'No active policy found', ); _enrollmentViewData = EnrollmentOverviewViewData.empty(); _isLoading = false; _isRefreshing = false; }); return; } final policyId = _selectedPolicyId!; final response = await _apiService.getClaimsCollectionV2All( clientPolicyId: policyId, ); final enrollmentResponse = await _apiService.getEnrollmentCollectionV1All( clientPolicyId: policyId, ); if (!mounted) return; final claimsOk = ClaimsKpiParser.isSuccessResponse(response); var claimsData = await ClaimsOverviewViewData.enrichFromApiResponse( response, fetchKpi: (slug) => _apiService.getClaimsCollectionV2Kpi( slug, clientPolicyId: policyId, ), ); 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 (!mounted) return; setState(() { _viewData = claimsData; _enrollmentViewData = enrollmentData; _isLoading = false; _isRefreshing = false; _replayToken++; }); ToastHelper.showErrorToast(context, message); return; } } if (!mounted) return; setState(() { _viewData = claimsData; _enrollmentViewData = enrollmentData; _isLoading = false; _isRefreshing = false; _replayToken++; }); } catch (e) { if (!mounted) return; setState(() { _viewData = ClaimsOverviewViewData.empty(error: e.toString()); _enrollmentViewData = EnrollmentOverviewViewData.empty(error: e.toString()); _isLoading = false; _isRefreshing = false; }); ToastHelper.showErrorToast(context, 'Failed to load claims overview'); } } 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 || _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 ?? '', clientLogoBytes: _clientLogoBytes, onProgress: (_, __, label) { updateProgressDialog?.call(() { progressLabel = label; }); }, ); 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); } } @override void dispose() { _tabController.removeListener(_onTabChanged); _tabController.dispose(); _scrollController.dispose(); _headerFadeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { 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( 'Real-time policy & claims analytics', style: GoogleFonts.poppins( fontSize: 13, color: ClaimsOverviewTheme.textSecondary, ), ), if (_viewData.liveUpdatedAt != null && _viewData.liveUpdatedAt!.isNotEmpty) ...[ const SizedBox(width: 12), ClaimsLiveIndicator( createdAt: _viewData.liveUpdatedAt, ), ], ], ), ], ), ), _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, ), ), const SizedBox(width: 4), ClaimsRefreshIcon( isRefreshing: _isRefreshing, onPressed: _loadDashboard, ), ], ), ), ); } 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, ), ClaimsRefreshIcon( isRefreshing: _isRefreshing, onPressed: _loadDashboard, ), ], ), ), ); }, 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() { return Padding( padding: const EdgeInsets.only(left: 4), child: IconButton( tooltip: 'Download all charts as PDF', icon: _isExportingPdf ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.download_outlined, size: 20), style: IconButton.styleFrom( foregroundColor: ClaimsOverviewTheme.textSecondary, ), onPressed: (_isRefreshing || _isExportingPdf || _isLoading) ? null : _downloadChartsPdf, ), ); } 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'), ], ), ), ), ); } }