diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index e6306aa..fb80f31 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -1069,8 +1069,8 @@ Files backup/restore - mirror mode or zip --> - [x] Add status system badges (✓ Complete, ⚠ Pending, ! Missing, ○ Archived) across document lists. ## Priority 4: Action-Oriented Dashboard -- [ ] Add "Needs Attention" operational card (missing SF9/SF10, documents requiring verification). -- [ ] Add simplified recent operational activity feed linking directly to student targets. +- [x] Add "Needs Attention" operational card (missing SF9/SF10, documents requiring verification). +- [x] Add simplified recent operational activity feed linking directly to student targets. ## Priority 5: OCR / Import Review Flow - [ ] Implement 4-step wizard UI: Select Files → Processing → Review & Match Results → Confirm Commit. diff --git a/frontend/lib/ui/screens/dashboard/dashboard_screen.dart b/frontend/lib/ui/screens/dashboard/dashboard_screen.dart index 1f35642..25d981a 100644 --- a/frontend/lib/ui/screens/dashboard/dashboard_screen.dart +++ b/frontend/lib/ui/screens/dashboard/dashboard_screen.dart @@ -23,6 +23,8 @@ import '../settings/teacher_management_screen.dart'; import 'widgets/notification_dropdown.dart'; import '../../shared/modals/view_activity_modal.dart'; import 'widgets/dashboard_kpis.dart'; +import 'widgets/needs_attention_card.dart'; +import 'widgets/recent_activity_feed.dart'; import '../../shared/widgets/app_error_state.dart'; class DashboardScreen extends ConsumerStatefulWidget { @@ -380,6 +382,30 @@ class _DashboardScreenState extends ConsumerState { ), const SizedBox(height: 24), _buildStatGrid(data.stats, user), + const SizedBox(height: 28), + LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth >= 900) { + return const Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 6, child: NeedsAttentionCard()), + SizedBox(width: 20), + Expanded(flex: 5, child: RecentActivityFeed()), + ], + ); + } else { + return const Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + NeedsAttentionCard(), + SizedBox(height: 20), + RecentActivityFeed(), + ], + ); + } + }, + ), if (isAdmin || data.stats.hasAssignedSections) ...[ const SizedBox(height: 32), const DashboardKpisSection(), diff --git a/frontend/lib/ui/screens/dashboard/widgets/needs_attention_card.dart b/frontend/lib/ui/screens/dashboard/widgets/needs_attention_card.dart new file mode 100644 index 0000000..1c409b3 --- /dev/null +++ b/frontend/lib/ui/screens/dashboard/widgets/needs_attention_card.dart @@ -0,0 +1,689 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../core/constants/app_colors.dart'; +import '../../../providers/dashboard_provider.dart'; +import '../../../providers/student_provider.dart'; +import '../../../providers/document_provider.dart'; +import '../../../providers/navigation_provider.dart'; +import '../../../providers/auth_provider.dart'; +import '../../documents/widgets/student_profile_modal.dart'; +import '../../documents/widgets/upload_ocr_modal.dart'; +import '../../../../domain/entities/student_model.dart'; +import '../../../../domain/entities/dashboard_models.dart'; + +/// "Needs Attention" operational card for Dashboard (Priority 4). +/// Shows missing SF9, missing SF10, documents requiring verification, +/// and immediate action links directly to student targets. +class NeedsAttentionCard extends ConsumerWidget { + const NeedsAttentionCard({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final user = ref.watch(authProvider).value; + final userRole = user?.role ?? 'teacher'; + + final kpisAsync = ref.watch(dashboardKpisProvider); + final studentsAsync = ref.watch(enrolledStudentsForRequirementsProvider); + + final enrolledStudents = studentsAsync.asData?.value ?? []; + final kpis = kpisAsync.asData?.value; + + // 1. Calculate missing SF9 count + final missingSf9Students = enrolledStudents.where((s) { + return s.missingDocuments.any( + (d) => d.toUpperCase().contains('SF9') || d.toUpperCase().contains('FORM 9'), + ); + }).toList(); + final sf9Count = missingSf9Students.isNotEmpty + ? missingSf9Students.length + : (kpis?.bottomStudents + .where((s) => s.missingRequirements.any((d) => d.toUpperCase().contains('SF9'))) + .length ?? + 0); + + // 2. Calculate missing SF10 count + final missingSf10Students = enrolledStudents.where((s) { + return s.missingDocuments.any( + (d) => d.toUpperCase().contains('SF10') || d.toUpperCase().contains('FORM 10'), + ); + }).toList(); + final sf10Count = missingSf10Students.isNotEmpty + ? missingSf10Students.length + : (kpis?.bottomStudents + .where((s) => s.missingRequirements.any((d) => d.toUpperCase().contains('SF10'))) + .length ?? + 0); + + // 3. Documents to verify (Pending status) + final pendingCount = kpis?.statusDistribution + .firstWhere( + (e) => e.status.toLowerCase() == 'pending', + orElse: () => const StatusDistributionEntry(status: 'Pending', count: 0), + ) + .count ?? + 0; + + final totalAlerts = sf9Count + sf10Count + pendingCount; + + // Top students needing immediate attention (most missing docs) + final urgentStudents = enrolledStudents + .where((s) => s.missingDocumentsCount > 0 || s.missingDocuments.isNotEmpty) + .toList() + ..sort((a, b) { + final countA = a.missingDocumentsCount > 0 ? a.missingDocumentsCount : a.missingDocuments.length; + final countB = b.missingDocumentsCount > 0 ? b.missingDocumentsCount : b.missingDocuments.length; + return countB.compareTo(countA); + }); + final displayedStudents = urgentStudents.take(3).toList(); + + return Container( + decoration: BoxDecoration( + color: isDark ? AppColors.darkSurfaceCard : Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: totalAlerts > 0 + ? (isDark ? AppColors.darkBorder : Colors.orange.shade200.withValues(alpha: 0.6)) + : (isDark ? AppColors.darkBorder : Colors.grey.shade200), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: isDark ? 0.25 : 0.04), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Header ── + Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 12), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(7), + decoration: BoxDecoration( + color: totalAlerts > 0 + ? Colors.orange.withValues(alpha: isDark ? 0.25 : 0.12) + : AppColors.primaryGreen.withValues(alpha: isDark ? 0.25 : 0.12), + borderRadius: BorderRadius.circular(9), + ), + child: Icon( + totalAlerts > 0 ? Icons.warning_amber_rounded : Icons.check_circle_outline_rounded, + color: totalAlerts > 0 ? Colors.orange.shade800 : AppColors.primaryGreen, + size: 20, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Needs Attention', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : AppColors.textPrimary, + ), + ), + if (totalAlerts > 0) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: isDark ? 0.3 : 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '$totalAlerts items', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: Colors.orange.shade800, + ), + ), + ), + ], + ], + ), + const SizedBox(height: 1), + Text( + totalAlerts > 0 + ? 'Operational items requiring action' + : 'All document requirements compliant', + style: TextStyle( + fontSize: 12, + color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, + ), + ), + ], + ), + ), + TextButton( + onPressed: () { + ref.read(activeTabProvider.notifier).setTab('Documents'); + }, + style: TextButton.styleFrom( + foregroundColor: AppColors.primaryGreen, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + minimumSize: const Size(0, 32), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'View all', + style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.bold), + ), + SizedBox(width: 4), + Icon(Icons.arrow_forward_rounded, size: 14), + ], + ), + ), + ], + ), + ), + const Divider(height: 1), + + // ── Operational Alert Rows ── + if (totalAlerts > 0) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + children: [ + // SF9 alert + _buildAlertRow( + context: context, + icon: Icons.assignment_late_outlined, + iconColor: Colors.orange.shade800, + label: '$sf9Count missing SF9', + sublabel: 'Learner Progress Report Card missing for enrolled students', + actionLabel: 'View', + isDark: isDark, + onAction: () => _openMissingDocSheet( + context, + title: 'Students Missing SF9', + students: missingSf9Students, + userRole: userRole, + isDark: isDark, + ), + ), + const SizedBox(height: 8), + + // SF10 alert + _buildAlertRow( + context: context, + icon: Icons.description_outlined, + iconColor: Colors.amber.shade800, + label: '$sf10Count missing SF10', + sublabel: 'Learner Permanent Academic Record missing', + actionLabel: 'View', + isDark: isDark, + onAction: () => _openMissingDocSheet( + context, + title: 'Students Missing SF10', + students: missingSf10Students, + userRole: userRole, + isDark: isDark, + ), + ), + const SizedBox(height: 8), + + // Pending verification alert + _buildAlertRow( + context: context, + icon: Icons.verified_outlined, + iconColor: const Color(0xFF1565C0), + label: '$pendingCount documents to verify', + sublabel: 'Uploaded records awaiting administrative review & verification', + actionLabel: 'Review', + isDark: isDark, + onAction: () { + ref.read(documentQueryProvider.notifier).setStatus('Pending'); + ref.invalidate(documentPageProvider); + ref.read(activeTabProvider.notifier).setTab('Documents'); + }, + ), + ], + ), + ), + + // ── Urgent Student Spotlight ── + if (displayedStudents.isNotEmpty) ...[ + const Divider(height: 1), + Padding( + padding: const EdgeInsets.fromLTRB(16, 10, 16, 6), + child: Row( + children: [ + Text( + 'STUDENTS NEEDING ATTENTION', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + color: isDark ? Colors.white54 : Colors.grey.shade600, + ), + ), + ], + ), + ), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + itemCount: displayedStudents.length, + separatorBuilder: (_, _) => const SizedBox(height: 6), + itemBuilder: (context, i) { + final s = displayedStudents[i]; + return _buildStudentTile( + context: context, + student: s, + userRole: userRole, + isDark: isDark, + ); + }, + ), + ], + ] else ...[ + // All compliant state + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + child: Center( + child: Column( + children: [ + Icon( + Icons.task_alt_rounded, + size: 36, + color: AppColors.primaryGreen.withValues(alpha: 0.8), + ), + const SizedBox(height: 8), + Text( + 'All records in good standing', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : AppColors.textPrimary, + ), + ), + const SizedBox(height: 2), + Text( + 'No missing SF9/SF10 or pending verification queues detected.', + style: TextStyle( + fontSize: 12, + color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, + ), + ), + ], + ), + ), + ), + ], + ], + ), + ); + } + + Widget _buildAlertRow({ + required BuildContext context, + required IconData icon, + required Color iconColor, + required String label, + required String sublabel, + required String actionLabel, + required bool isDark, + required VoidCallback onAction, + }) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onAction, + borderRadius: BorderRadius.circular(10), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), + decoration: BoxDecoration( + color: iconColor.withValues(alpha: isDark ? 0.12 : 0.06), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: iconColor.withValues(alpha: isDark ? 0.25 : 0.18), + width: 0.8, + ), + ), + child: Row( + children: [ + Icon(icon, size: 18, color: iconColor), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : AppColors.textPrimary, + ), + ), + Text( + sublabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11, + color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: iconColor, + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + actionLabel, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(width: 2), + const Icon(Icons.arrow_forward, size: 11, color: Colors.white), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildStudentTile({ + required BuildContext context, + required StudentModel student, + required String userRole, + required bool isDark, + }) { + final missingList = student.missingDocuments.take(2).toList(); + final extraCount = student.missingDocuments.length - missingList.length; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: BoxDecoration( + color: isDark ? AppColors.darkSurface2 : const Color(0xFFF8F9FA), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isDark ? AppColors.darkBorder : Colors.grey.shade200, + width: 0.8, + ), + ), + child: Row( + children: [ + CircleAvatar( + radius: 14, + backgroundColor: AppColors.primaryGreen.withValues(alpha: 0.15), + child: Text( + student.firstName.isNotEmpty ? student.firstName[0].toUpperCase() : 'S', + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: AppColors.primaryGreen, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + student.listDisplayName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.5, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : AppColors.textPrimary, + ), + ), + const SizedBox(height: 2), + Row( + children: [ + for (final doc in missingList) ...[ + Container( + margin: const EdgeInsets.only(right: 4), + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '! $doc', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.red.shade800, + ), + ), + ), + ], + if (extraCount > 0) ...[ + Text( + '+$extraCount more', + style: TextStyle( + fontSize: 10, + color: isDark ? Colors.white54 : Colors.black54, + ), + ), + ], + ], + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.cloud_upload_outlined, size: 18), + tooltip: 'Upload Document', + color: AppColors.primaryGreen, + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + padding: EdgeInsets.zero, + onPressed: () { + UploadOcrModal.show( + context, + prefilledStudentId: student.id, + prefilledLrn: student.lrn, + ); + }, + ), + IconButton( + icon: const Icon(Icons.person_outline_rounded, size: 18), + tooltip: 'View Profile', + color: isDark ? Colors.white70 : Colors.black87, + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + padding: EdgeInsets.zero, + onPressed: () { + showStudentProfileModal( + context, + studentId: student.id, + userRole: userRole, + hideEnrollmentActions: true, + ); + }, + ), + ], + ), + ); + } + + void _openMissingDocSheet( + BuildContext context, { + required String title, + required List students, + required String userRole, + required bool isDark, + }) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) { + return Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.75, + ), + decoration: BoxDecoration( + color: isDark ? AppColors.darkSurfaceCard : Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 10), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: isDark ? Colors.white24 : Colors.grey.shade300, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(7), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.15), + shape: BoxShape.circle, + ), + child: Icon( + Icons.warning_amber_rounded, + size: 20, + color: Colors.orange.shade800, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : AppColors.textPrimary, + ), + ), + Text( + '${students.length} students currently missing this record', + style: TextStyle( + fontSize: 12, + color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, + ), + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(ctx).pop(), + ), + ], + ), + ), + const Divider(height: 16), + if (students.isEmpty) + const Padding( + padding: EdgeInsets.all(32), + child: Center(child: Text('No students missing this requirement.')), + ) + else + Flexible( + child: ListView.separated( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + itemCount: students.length, + separatorBuilder: (_, _) => Divider( + height: 1, + color: isDark ? AppColors.darkBorder : Colors.grey.shade100, + ), + itemBuilder: (context, i) { + final s = students[i]; + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + title: Text( + s.listDisplayName, + style: TextStyle( + fontSize: 13.5, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : AppColors.textPrimary, + ), + ), + subtitle: Text( + 'LRN: ${s.lrn}', + style: TextStyle( + fontSize: 11.5, + color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.cloud_upload_outlined, size: 20), + tooltip: 'Upload Document', + color: AppColors.primaryGreen, + onPressed: () { + Navigator.of(ctx).pop(); + UploadOcrModal.show( + context, + prefilledStudentId: s.id, + prefilledLrn: s.lrn, + ); + }, + ), + IconButton( + icon: const Icon(Icons.person_outline_rounded, size: 20), + tooltip: 'View Profile', + color: isDark ? Colors.white70 : Colors.black87, + onPressed: () { + Navigator.of(ctx).pop(); + showStudentProfileModal( + context, + studentId: s.id, + userRole: userRole, + hideEnrollmentActions: true, + ); + }, + ), + ], + ), + ); + }, + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/frontend/lib/ui/screens/dashboard/widgets/recent_activity_feed.dart b/frontend/lib/ui/screens/dashboard/widgets/recent_activity_feed.dart new file mode 100644 index 0000000..501e967 --- /dev/null +++ b/frontend/lib/ui/screens/dashboard/widgets/recent_activity_feed.dart @@ -0,0 +1,345 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../core/constants/app_colors.dart'; +import '../../../../core/utils/date_utils.dart' as pht; +import '../../../providers/dashboard_provider.dart'; +import '../../../providers/auth_provider.dart'; +import '../../documents/widgets/student_profile_modal.dart'; +import '../../../shared/modals/view_activity_modal.dart'; +import '../recent_activities_screen.dart'; +import '../../../../domain/entities/dashboard_models.dart'; + +/// Simplified Recent Operational Activity Feed (Priority 4). +/// Displays recent document & student actions with human-readable format +/// and direct one-click links to student targets. +class RecentActivityFeed extends ConsumerWidget { + const RecentActivityFeed({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final user = ref.watch(authProvider).value; + final userRole = user?.role ?? 'teacher'; + + final dashboardAsync = ref.watch(dashboardDataProvider); + final activities = dashboardAsync.asData?.value.recentActivities.activities ?? []; + final displayedActivities = activities.take(5).toList(); + + return Container( + decoration: BoxDecoration( + color: isDark ? AppColors.darkSurfaceCard : Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isDark ? AppColors.darkBorder : Colors.grey.shade200, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: isDark ? 0.25 : 0.04), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Header ── + Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 12), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(7), + decoration: BoxDecoration( + color: AppColors.primaryGreen.withValues(alpha: isDark ? 0.25 : 0.12), + borderRadius: BorderRadius.circular(9), + ), + child: const Icon( + Icons.history_rounded, + color: AppColors.primaryGreen, + size: 20, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Recent Activity', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : AppColors.textPrimary, + ), + ), + const SizedBox(height: 1), + Text( + 'Operational document & student audit trail', + style: TextStyle( + fontSize: 12, + color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, + ), + ), + ], + ), + ), + TextButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const RecentActivitiesScreen(), + ), + ); + }, + style: TextButton.styleFrom( + foregroundColor: AppColors.primaryGreen, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + minimumSize: const Size(0, 32), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Full History', + style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.bold), + ), + SizedBox(width: 4), + Icon(Icons.arrow_forward_rounded, size: 14), + ], + ), + ), + ], + ), + ), + const Divider(height: 1), + + // ── Activity List ── + if (displayedActivities.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 32), + child: Center( + child: Column( + children: [ + Icon( + Icons.inbox_outlined, + size: 36, + color: isDark ? Colors.white30 : Colors.grey.shade400, + ), + const SizedBox(height: 8), + Text( + 'No recent activity recorded', + style: TextStyle( + fontSize: 13, + color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, + ), + ), + ], + ), + ), + ) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + itemCount: displayedActivities.length, + separatorBuilder: (_, _) => Divider( + height: 1, + color: isDark ? AppColors.darkBorder : Colors.grey.shade100, + ), + itemBuilder: (context, i) { + final activity = displayedActivities[i]; + return _buildActivityItem( + context: context, + activity: activity, + userRole: userRole, + isDark: isDark, + ); + }, + ), + ], + ), + ); + } + + Widget _buildActivityItem({ + required BuildContext context, + required RecentActivity activity, + required String userRole, + required bool isDark, + }) { + final meta = _getActivityMeta(activity); + final relativeTime = pht.formatRelative(activity.createdAt); + final isStudentTarget = activity.entityType.toLowerCase() == 'student' && activity.entityId != null; + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + if (isStudentTarget) { + showStudentProfileModal( + context, + studentId: activity.entityId!, + userRole: userRole, + hideEnrollmentActions: true, + ); + } else { + ViewActivityModal.show( + context: context, + title: activity.action.toUpperCase(), + description: activity.description, + date: activity.createdAt, + icon: meta.icon, + actionColor: meta.color, + ); + } + }, + borderRadius: BorderRadius.circular(10), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9), + child: Row( + children: [ + // Event icon + Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: meta.color.withValues(alpha: isDark ? 0.20 : 0.10), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(meta.icon, size: 17, color: meta.color), + ), + const SizedBox(width: 10), + + // Title and human-readable narrative + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + meta.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : AppColors.textPrimary, + ), + ), + ), + const SizedBox(width: 6), + Text( + relativeTime, + style: TextStyle( + fontSize: 11, + color: isDark ? AppColors.darkTextMuted : AppColors.textMuted, + ), + ), + ], + ), + const SizedBox(height: 2), + Row( + children: [ + Expanded( + child: Text( + meta.subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11.5, + color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, + ), + ), + ), + if (activity.performedBy != null || activity.username != null) ...[ + const SizedBox(width: 4), + Text( + '• by ${activity.performedBy ?? activity.username}', + style: TextStyle( + fontSize: 10.5, + fontStyle: FontStyle.italic, + color: isDark ? Colors.white38 : Colors.grey.shade600, + ), + ), + ], + ], + ), + ], + ), + ), + + const SizedBox(width: 6), + Icon( + Icons.chevron_right, + size: 16, + color: isDark ? Colors.white30 : Colors.grey.shade400, + ), + ], + ), + ), + ), + ); + } + + _ActivityMeta _getActivityMeta(RecentActivity activity) { + final act = activity.action.toUpperCase(); + final type = activity.entityType.toLowerCase(); + final desc = activity.description; + + // Determine icon and color + IconData icon = Icons.info_outline; + Color color = AppColors.primaryGreen; + + if (act.contains('UPLOAD') || desc.toLowerCase().contains('upload')) { + icon = Icons.cloud_upload_outlined; + color = AppColors.primaryGreen; + } else if (act.contains('VERIF') || desc.toLowerCase().contains('verif')) { + icon = Icons.verified_outlined; + color = const Color(0xFF1565C0); + } else if (act.contains('DELETE') || desc.toLowerCase().contains('delet')) { + icon = Icons.delete_outline_rounded; + color = AppColors.error; + } else if (act.contains('CREATE') || act.contains('ENROLL')) { + icon = Icons.add_circle_outline_rounded; + color = Colors.teal; + } else if (act.contains('UPDATE') || act.contains('EDIT')) { + icon = Icons.edit_note_rounded; + color = Colors.orange.shade800; + } else if (type == 'student') { + icon = Icons.person_outline_rounded; + color = Colors.indigo; + } + + // Determine clean title and subtitle + String title = desc; + String subtitle = '$act • ${type.toUpperCase()}'; + + // Parse natural phrasing from description + if (desc.isNotEmpty) { + title = desc; + subtitle = '$act on $type'; + } + + return _ActivityMeta(icon: icon, color: color, title: title, subtitle: subtitle); + } +} + +class _ActivityMeta { + final IconData icon; + final Color color; + final String title; + final String subtitle; + + _ActivityMeta({ + required this.icon, + required this.color, + required this.title, + required this.subtitle, + }); +}