import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/theme/app_theme.dart'; import '../../models/attendance.dart'; import '../../models/dashboard.dart'; import '../../models/fee.dart'; import '../../providers/auth_provider.dart'; import '../../providers/dashboard_provider.dart'; class DashboardScreen extends ConsumerWidget { const DashboardScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final dashboardAsync = ref.watch(dashboardProvider); final authState = ref.watch(authProvider); return Scaffold( appBar: AppBar( title: const Text('Student Portal'), actions: [ IconButton( icon: const Icon(Icons.settings_outlined), tooltip: 'Settings', onPressed: () => context.push('/settings'), ), IconButton( icon: const Icon(Icons.logout_rounded), tooltip: 'Sign Out', onPressed: () async { final confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Sign Out'), content: const Text('Are you sure you want to sign out?'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Cancel'), ), ElevatedButton( onPressed: () => Navigator.pop(context, true), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.errorColor, ), child: const Text('Sign Out'), ), ], ), ); if (confirm == true) { await ref.read(authProvider.notifier).logout(); if (context.mounted) { context.go('/login'); } } }, ), ], ), body: RefreshIndicator( onRefresh: () async { ref.invalidate(dashboardProvider); await ref.read(dashboardProvider.future); }, child: dashboardAsync.when( loading: () => const Center( child: CircularProgressIndicator(), ), error: (err, stack) => Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.error_outline_rounded, size: 48, color: AppTheme.errorColor, ), const SizedBox(height: 16), const Text( 'Failed to load dashboard data', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Text( err.toString(), textAlign: TextAlign.center, style: const TextStyle( color: AppTheme.textSecondary, fontSize: 13, ), ), const SizedBox(height: 20), ElevatedButton.icon( onPressed: () => ref.refresh(dashboardProvider), icon: const Icon(Icons.refresh, size: 18), label: const Text('Try Again'), ), ], ), ), ), data: (data) => _buildDashboardContent(context, data, authState.student?.name), ), ), bottomNavigationBar: NavigationBar( selectedIndex: 0, backgroundColor: Colors.white, elevation: 2, onDestinationSelected: (index) { switch (index) { case 0: // Already on dashboard break; case 1: context.push('/attendance'); break; case 2: context.push('/timetable'); break; case 3: context.push('/exams'); break; case 4: context.push('/profile'); break; } }, destinations: const [ NavigationDestination( icon: Icon(Icons.dashboard_outlined), selectedIcon: Icon(Icons.dashboard_rounded, color: AppTheme.primaryColor), label: 'Home', ), NavigationDestination( icon: Icon(Icons.fact_check_outlined), selectedIcon: Icon(Icons.fact_check_rounded, color: AppTheme.primaryColor), label: 'Attendance', ), NavigationDestination( icon: Icon(Icons.calendar_today_outlined), selectedIcon: Icon(Icons.calendar_today_rounded, color: AppTheme.primaryColor), label: 'Schedule', ), NavigationDestination( icon: Icon(Icons.quiz_outlined), selectedIcon: Icon(Icons.quiz_rounded, color: AppTheme.primaryColor), label: 'Exams', ), NavigationDestination( icon: Icon(Icons.person_outline_rounded), selectedIcon: Icon(Icons.person_rounded, color: AppTheme.primaryColor), label: 'Profile', ), ], ), ); } Widget _buildDashboardContent( BuildContext context, DashboardData data, String? fallbackName, ) { final studentName = data.student?.fullName.isNotEmpty == true ? data.student!.fullName : (fallbackName ?? 'Student'); final programName = data.student?.program?.name ?? 'Enrolled Student'; return ListView( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), children: [ // Student Info Banner Card InkWell( onTap: () => context.push('/profile'), borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: const LinearGradient( colors: [AppTheme.primaryDark, AppTheme.primaryColor], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ CircleAvatar( radius: 28, backgroundColor: Colors.white.withOpacity(0.2), child: Text( studentName.isNotEmpty ? studentName[0].toUpperCase() : 'S', style: const TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, ), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( studentName, style: const TextStyle( color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( programName, style: TextStyle( color: Colors.white.withOpacity(0.85), fontSize: 13, ), ), ], ), ), const Icon( Icons.arrow_forward_ios_rounded, size: 16, color: Colors.white70, ), ], ), ), ), const SizedBox(height: 16), // Quick Attendance Card _buildAttendanceCard(context, data.attendance), const SizedBox(height: 16), // Quick Fee Card if (data.fees != null) ...[ _buildFeeCard(context, data.fees!), const SizedBox(height: 16), ], // Today's Timetable Section _buildTimetableSection(context, data.todayTimetable), const SizedBox(height: 16), // Upcoming Exams Section _buildExamsSection(context, data.upcomingExams), const SizedBox(height: 16), // Recent Results Section _buildResultsSection(context, data.recentResults), const SizedBox(height: 16), // Announcements Section if (data.announcements.isNotEmpty) ...[ _buildAnnouncementsSection(context, data.announcements), const SizedBox(height: 20), ], ], ); } Widget _buildAttendanceCard(BuildContext context, AttendanceSummary summary) { return Card( child: InkWell( onTap: () => context.push('/attendance'), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Attendance Overview', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), Text( 'View Details →', style: TextStyle( fontSize: 13, color: AppTheme.primaryLight, fontWeight: FontWeight.w500, ), ), ], ), const SizedBox(height: 14), Row( children: [ // Attendance Percentage Circular Indicator SizedBox( width: 64, height: 64, child: Stack( fit: StackFit.expand, children: [ CircularProgressIndicator( value: (summary.percentage / 100).clamp(0.0, 1.0), backgroundColor: AppTheme.borderLight, color: summary.percentage >= 75 ? AppTheme.successColor : (summary.percentage >= 60 ? AppTheme.warningColor : AppTheme.errorColor), strokeWidth: 6, ), Center( child: Text( '${summary.percentage.toStringAsFixed(1)}%', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), ), ], ), ), const SizedBox(width: 24), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildAttendanceMetric('Present', summary.present, AppTheme.successColor), _buildAttendanceMetric('Absent', summary.absent, AppTheme.errorColor), _buildAttendanceMetric('Late', summary.late, AppTheme.warningColor), ], ), ), ], ), ], ), ), ), ); } Widget _buildAttendanceMetric(String label, int count, Color color) { return Column( children: [ Text( '$count', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: color, ), ), const SizedBox(height: 2), Text( label, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), ], ); } Widget _buildFeeCard(BuildContext context, DashboardFeeSummary fees) { Color statusColor; String statusLabel; switch (fees.paymentStatus) { case 'fully_paid': case 'paid': statusColor = AppTheme.successColor; statusLabel = 'Paid'; break; case 'overdue': statusColor = AppTheme.errorColor; statusLabel = 'Overdue'; break; case 'partial': statusColor = AppTheme.warningColor; statusLabel = 'Partial'; break; default: statusColor = fees.outstandingBalance > 0 ? AppTheme.primaryLight : AppTheme.successColor; statusLabel = fees.outstandingBalance > 0 ? 'Pending' : 'No Dues'; } final sym = fees.currencySymbol; return Card( child: InkWell( onTap: () => context.push('/fees'), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Row( children: [ Icon(Icons.account_balance_wallet_outlined, size: 18, color: AppTheme.primaryColor), SizedBox(width: 8), Text( 'Fee Management', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), ], ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: statusColor.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(6), border: Border.all(color: statusColor.withValues(alpha: 0.5)), ), child: Text( statusLabel, style: TextStyle( color: statusColor, fontWeight: FontWeight.bold, fontSize: 11, ), ), ), ], ), const SizedBox(height: 14), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Outstanding Dues', style: TextStyle(fontSize: 12, color: AppTheme.textSecondary), ), const SizedBox(height: 4), Text( '$sym${fees.outstandingBalance.toStringAsFixed(2)}', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: fees.overdueAmount > 0 ? AppTheme.errorColor : AppTheme.textPrimary, ), ), ], ), const Row( children: [ Text( 'View Invoices →', style: TextStyle( fontSize: 13, color: AppTheme.primaryLight, fontWeight: FontWeight.w500, ), ), ], ), ], ), if (fees.nextDueDate != null) ...[ const SizedBox(height: 10), Text( 'Next Due Date: ${fees.nextDueDate}', style: const TextStyle(fontSize: 11, color: AppTheme.textMuted), ), ], ], ), ), ), ); } Widget _buildTimetableSection(BuildContext context, List todaySlots) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( "Today's Schedule", style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), TextButton( onPressed: () => context.push('/timetable'), style: TextButton.styleFrom( padding: EdgeInsets.zero, minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: const Text('Full Schedule →'), ), ], ), const SizedBox(height: 12), if (todaySlots.isEmpty) Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 20), decoration: BoxDecoration( color: AppTheme.surfaceColor, borderRadius: BorderRadius.circular(8), ), child: const Column( children: [ Icon(Icons.event_available_rounded, size: 28, color: AppTheme.textMuted), SizedBox(height: 6), Text( 'No classes scheduled today', style: TextStyle(color: AppTheme.textSecondary, fontSize: 13), ), ], ), ) else ...todaySlots.map( (slot) => Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: AppTheme.surfaceColor, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: AppTheme.primaryColor.withOpacity(0.08), borderRadius: BorderRadius.circular(6), ), child: Text( '${slot.startTime} - ${slot.endTime}', style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppTheme.primaryColor, ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( slot.subject?.name ?? 'Class Session', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, ), ), if (slot.room?.name != null) Text( slot.room!.name, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), ], ), ), ], ), ), ), ], ), ), ); } Widget _buildExamsSection(BuildContext context, List upcomingExams) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Upcoming Exams', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), TextButton( onPressed: () => context.push('/exams'), style: TextButton.styleFrom( padding: EdgeInsets.zero, minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: const Text('View All →'), ), ], ), const SizedBox(height: 12), if (upcomingExams.isEmpty) Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 20), decoration: BoxDecoration( color: AppTheme.surfaceColor, borderRadius: BorderRadius.circular(8), ), child: const Column( children: [ Icon(Icons.quiz_outlined, size: 28, color: AppTheme.textMuted), SizedBox(height: 6), Text( 'No upcoming exams', style: TextStyle(color: AppTheme.textSecondary, fontSize: 13), ), ], ), ) else ...upcomingExams.map( (exam) => ListTile( dense: true, contentPadding: EdgeInsets.zero, leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppTheme.secondaryColor.withOpacity(0.1), shape: BoxShape.circle, ), child: const Icon(Icons.edit_calendar, size: 18, color: AppTheme.secondaryColor), ), title: Text( exam.name, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13), ), subtitle: Text( exam.startDate != null ? 'Date: ${exam.startDate}' : 'Scheduled', style: const TextStyle(fontSize: 12), ), trailing: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Text( exam.state.toUpperCase(), style: const TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: Colors.blue, ), ), ), ), ), ], ), ), ); } Widget _buildResultsSection(BuildContext context, List recentResults) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Recent Results', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), TextButton( onPressed: () => context.push('/results'), style: TextButton.styleFrom( padding: EdgeInsets.zero, minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: const Text('View All →'), ), ], ), const SizedBox(height: 12), if (recentResults.isEmpty) Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 20), decoration: BoxDecoration( color: AppTheme.surfaceColor, borderRadius: BorderRadius.circular(8), ), child: const Column( children: [ Icon(Icons.grade_outlined, size: 28, color: AppTheme.textMuted), SizedBox(height: 6), Text( 'No recent results', style: TextStyle(color: AppTheme.textSecondary, fontSize: 13), ), ], ), ) else ...recentResults.map( (result) => ListTile( dense: true, contentPadding: EdgeInsets.zero, leading: CircleAvatar( radius: 16, backgroundColor: result.status.toLowerCase() == 'pass' ? AppTheme.successColor.withOpacity(0.1) : AppTheme.errorColor.withOpacity(0.1), child: Text( result.grade ?? (result.status.toLowerCase() == 'pass' ? 'P' : 'F'), style: TextStyle( color: result.status.toLowerCase() == 'pass' ? AppTheme.successColor : AppTheme.errorColor, fontWeight: FontWeight.bold, fontSize: 12, ), ), ), title: Text( result.exam?.name ?? 'Exam Result', style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13), ), subtitle: Text( 'Score: ${result.percentage.toStringAsFixed(1)}%', style: const TextStyle(fontSize: 12), ), trailing: Text( result.status.toUpperCase(), style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: result.status.toLowerCase() == 'pass' ? AppTheme.successColor : AppTheme.errorColor, ), ), ), ), ], ), ), ); } Widget _buildAnnouncementsSection(BuildContext context, List announcements) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Row( children: [ Icon(Icons.campaign_outlined, color: AppTheme.primaryColor, size: 20), SizedBox(width: 8), Text( 'Announcements', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), ], ), TextButton( onPressed: () => context.push('/announcements'), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Text('View All', style: TextStyle(fontSize: 12)), Icon(Icons.chevron_right, size: 16), ], ), ), ], ), const SizedBox(height: 8), ...announcements.map( (ann) => InkWell( borderRadius: BorderRadius.circular(8), onTap: () => context.push('/announcements'), child: Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.surfaceColor, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( ann.name, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.bold, ), ), ), if (ann.date.isNotEmpty) Text( ann.date, style: const TextStyle( fontSize: 11, color: AppTheme.textSecondary, ), ), ], ), if (ann.summary.isNotEmpty) ...[ const SizedBox(height: 4), Text( ann.summary, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), ], ], ), ), ), ), ], ), ), ); } }