896 lines
31 KiB
Dart
896 lines
31 KiB
Dart
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<bool>(
|
|
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<DashboardExam> 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<DashboardResult> 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<Announcement> 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,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|