From 502b2ea9f1d5eb9eefce8588fde4908f45cbef87 Mon Sep 17 00:00:00 2001 From: ssbg04 Date: Sun, 11 Oct 2026 20:13:27 +0800 Subject: [PATCH] feat(dashboard): isolate needs attention into dedicated endpoint and provider --- frontend/AGENTS.md | 3 ++- .../repositories/dashboard_repository.dart | 11 +++++++++++ frontend/lib/ui/providers/auth_provider.dart | 3 +++ .../lib/ui/providers/dashboard_provider.dart | 12 ++++++++++++ .../ui/screens/dashboard/dashboard_screen.dart | 3 +++ .../dashboard/widgets/needs_attention_card.dart | 16 +++++++--------- 6 files changed, 38 insertions(+), 10 deletions(-) diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 0c8699f..4ae4541 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -1109,7 +1109,7 @@ Files backup/restore - mirror mode or zip --> - [x] Removed "Top Students by Documents" and "Needs Attention" cards from the bottom of the dashboard. - [x] Fixed "4 missing" empty list bug: student count and student modal list are derived synchronously and directly from matching students. - [x] Implemented dynamic missing document requirement chips (Approach A): dynamically evaluates and sorts missing counts across all enabled mandatory requirements (SF9, SF10, PSA, etc.) descending by missing frequency. -- [x] Needs Attention section fix: synchronized backend KPI endpoint (`/api/dashboard/kpis`) to return exact mandatory missing document requirements, counts, and urgent student records for both Admin and Teacher roles; updated frontend models and `NeedsAttentionCard` to consume this synced dataset with direct profile and upload actions. +- [x] Needs Attention section fix: created dedicated endpoint (`/api/dashboard/needs-attention`) filtering strictly active enrolled students (`s.status = 'Enrolled'`) and dedicated `dashboardNeedsAttentionProvider` to isolate alerts from other KPI chart metrics. # STUDENT SCREEN - enrolled tab: @@ -1128,6 +1128,7 @@ Files backup/restore - mirror mode or zip --> - [x] Removed Recycle Bin, delete, restore, upload new version, and copy buttons from the Archive Screen (including folder context menus, document context menus, document preview modal, and bulk operations bar). + --- diff --git a/frontend/lib/domain/repositories/dashboard_repository.dart b/frontend/lib/domain/repositories/dashboard_repository.dart index 7477b88..413704a 100644 --- a/frontend/lib/domain/repositories/dashboard_repository.dart +++ b/frontend/lib/domain/repositories/dashboard_repository.dart @@ -66,4 +66,15 @@ class DashboardRepository { throw Exception(e.response?.data['message'] ?? 'Failed to fetch KPIs.'); } } + + Future getNeedsAttention() async { + try { + final options = await _getAuthOptions(); + final response = await _dio.get('/dashboard/needs-attention', options: options); + return NeedsAttentionData.fromJson(response.data as Map); + } on DioException catch (e) { + throw Exception(e.response?.data['message'] ?? 'Failed to fetch Needs Attention data.'); + } + } } + diff --git a/frontend/lib/ui/providers/auth_provider.dart b/frontend/lib/ui/providers/auth_provider.dart index 61eaee0..9affe36 100644 --- a/frontend/lib/ui/providers/auth_provider.dart +++ b/frontend/lib/ui/providers/auth_provider.dart @@ -42,6 +42,7 @@ class AuthNotifier extends AsyncNotifier { ); ref.invalidate(dashboardDataProvider); ref.invalidate(dashboardKpisProvider); + ref.invalidate(dashboardNeedsAttentionProvider); state = AsyncData(user); ref.read(heartbeatServiceProvider).start(user); ForegroundSyncService.start(); @@ -105,8 +106,10 @@ class AuthNotifier extends AsyncNotifier { ref.invalidate(dashboardDataProvider); ref.invalidate(dashboardKpisProvider); + ref.invalidate(dashboardNeedsAttentionProvider); } + Future refreshUser() async { try { final repository = ref.read(authRepositoryProvider); diff --git a/frontend/lib/ui/providers/dashboard_provider.dart b/frontend/lib/ui/providers/dashboard_provider.dart index 05af592..fb0b05f 100644 --- a/frontend/lib/ui/providers/dashboard_provider.dart +++ b/frontend/lib/ui/providers/dashboard_provider.dart @@ -59,6 +59,18 @@ final dashboardKpisProvider = FutureProvider.autoDispose((ref) as return repository.getKpis(); }); +// ── Needs Attention dedicated provider ───────────────────────────────────── +final dashboardNeedsAttentionProvider = + FutureProvider.autoDispose((ref) async { + final user = ref.watch(authProvider).value; + if (user == null) { + throw Exception('User not authenticated'); + } + final repository = ref.watch(dashboardRepositoryProvider); + return repository.getNeedsAttention(); +}); + + // ── Full paginated activities (used by RecentActivitiesScreen) ───────────── class ActivityQueryParams { final int page; diff --git a/frontend/lib/ui/screens/dashboard/dashboard_screen.dart b/frontend/lib/ui/screens/dashboard/dashboard_screen.dart index 98e2697..d7173ff 100644 --- a/frontend/lib/ui/screens/dashboard/dashboard_screen.dart +++ b/frontend/lib/ui/screens/dashboard/dashboard_screen.dart @@ -139,6 +139,7 @@ class _DashboardScreenState extends ConsumerState { ref.read(activeTabProvider) == 'Dashboard') { ref.invalidate(dashboardDataProvider); ref.invalidate(dashboardKpisProvider); + ref.invalidate(dashboardNeedsAttentionProvider); ref.read(notificationsProvider.notifier).refreshNotifications(); } }); @@ -152,12 +153,14 @@ class _DashboardScreenState extends ConsumerState { if (!mounted) return; ref.invalidate(dashboardDataProvider); ref.invalidate(dashboardKpisProvider); + ref.invalidate(dashboardNeedsAttentionProvider); if (!mounted) return; ref.read(notificationsProvider.notifier).refreshNotifications(); if (!mounted) return; await ref.read(dashboardDataProvider.future); } + Color _getNotificationColor(String title) { final t = title.toLowerCase(); if (t.contains('student')) return Colors.blue; diff --git a/frontend/lib/ui/screens/dashboard/widgets/needs_attention_card.dart b/frontend/lib/ui/screens/dashboard/widgets/needs_attention_card.dart index 5ea59e2..fd98a12 100644 --- a/frontend/lib/ui/screens/dashboard/widgets/needs_attention_card.dart +++ b/frontend/lib/ui/screens/dashboard/widgets/needs_attention_card.dart @@ -63,9 +63,9 @@ class NeedsAttentionCard extends ConsumerWidget { final user = ref.watch(authProvider).value; final userRole = user?.role ?? 'teacher'; - final kpisAsync = ref.watch(dashboardKpisProvider); + final needsAttentionAsync = ref.watch(dashboardNeedsAttentionProvider); - if (kpisAsync.isLoading) { + if (needsAttentionAsync.isLoading) { return Container( height: 140, decoration: BoxDecoration( @@ -86,7 +86,7 @@ class NeedsAttentionCard extends ConsumerWidget { ); } - if (kpisAsync.hasError) { + if (needsAttentionAsync.hasError) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -113,7 +113,7 @@ class NeedsAttentionCard extends ConsumerWidget { ), const SizedBox(height: 6), TextButton( - onPressed: () => ref.invalidate(dashboardKpisProvider), + onPressed: () => ref.invalidate(dashboardNeedsAttentionProvider), child: const Text('Retry'), ), ], @@ -122,8 +122,7 @@ class NeedsAttentionCard extends ConsumerWidget { ); } - final kpis = kpisAsync.value; - final needsAttention = kpis?.needsAttention; + final needsAttention = needsAttentionAsync.value; // missing requirements from backend needsAttention final List missingGroups = needsAttention?.missingRequirements ?? []; @@ -131,11 +130,10 @@ class NeedsAttentionCard extends ConsumerWidget { missingGroups.fold(0, (sum, g) => sum + g.count); // Urgent students needing immediate attention - final List urgentStudents = needsAttention?.urgentStudents.isNotEmpty == true - ? needsAttention!.urgentStudents - : (kpis?.bottomStudents.where((s) => s.missingCount > 0).take(5).toList() ?? []); + final List urgentStudents = needsAttention?.urgentStudents ?? []; final displayedStudents = urgentStudents.take(3).toList(); + return Container( decoration: BoxDecoration( color: isDark ? AppColors.darkSurfaceCard : Colors.white,