feat(dashboard): isolate needs attention into dedicated endpoint and provider

This commit is contained in:
ssbg04 committed 2026-10-11 20:13:27 +08:00
1 parent 2416a598a1
commit 502b2ea9f1
6 files changed
+38 -10

No files matched your search

+2 -1
View File
@@ -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).
---
@@ -66,4 +66,15 @@ class DashboardRepository {
throw Exception(e.response?.data['message'] ?? 'Failed to fetch KPIs.');
}
}
Future<NeedsAttentionData> getNeedsAttention() async {
try {
final options = await _getAuthOptions();
final response = await _dio.get('/dashboard/needs-attention', options: options);
return NeedsAttentionData.fromJson(response.data as Map<String, dynamic>);
} on DioException catch (e) {
throw Exception(e.response?.data['message'] ?? 'Failed to fetch Needs Attention data.');
}
}
}
@@ -42,6 +42,7 @@ class AuthNotifier extends AsyncNotifier<UserModel?> {
);
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<UserModel?> {
ref.invalidate(dashboardDataProvider);
ref.invalidate(dashboardKpisProvider);
ref.invalidate(dashboardNeedsAttentionProvider);
}
Future<void> refreshUser() async {
try {
final repository = ref.read(authRepositoryProvider);
@@ -59,6 +59,18 @@ final dashboardKpisProvider = FutureProvider.autoDispose<DashboardKpis>((ref) as
return repository.getKpis();
});
// ── Needs Attention dedicated provider ─────────────────────────────────────
final dashboardNeedsAttentionProvider =
FutureProvider.autoDispose<NeedsAttentionData>((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;
@@ -139,6 +139,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
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<DashboardScreen> {
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;
@@ -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<MissingRequirementGroup> missingGroups = needsAttention?.missingRequirements ?? [];
@@ -131,11 +130,10 @@ class NeedsAttentionCard extends ConsumerWidget {
missingGroups.fold<int>(0, (sum, g) => sum + g.count);
// Urgent students needing immediate attention
final List<StudentDocCount> urgentStudents = needsAttention?.urgentStudents.isNotEmpty == true
? needsAttention!.urgentStudents
: (kpis?.bottomStudents.where((s) => s.missingCount > 0).take(5).toList() ?? []);
final List<StudentDocCount> urgentStudents = needsAttention?.urgentStudents ?? [];
final displayedStudents = urgentStudents.take(3).toList();
return Container(
decoration: BoxDecoration(
color: isDark ? AppColors.darkSurfaceCard : Colors.white,