diff --git a/backend/src/controllers/dashboardController.js b/backend/src/controllers/dashboardController.js index 4ef6f71..4d6c333 100644 --- a/backend/src/controllers/dashboardController.js +++ b/backend/src/controllers/dashboardController.js @@ -20,66 +20,63 @@ exports.getStats = (req, res) => { const activeAyId = activeAy?.id; if (isTeacher) { - // Count of students enrolled in teacher's assigned sections in the active academic year + // Count of students enrolled in teacher's assigned sections across all assigned sections totalStudents = db.prepare(` SELECT COUNT(DISTINCT s.id) as count FROM students s JOIN enrollments e ON s.id = e.student_id - WHERE e.academic_year_id = ? - AND e.section_id IN ( - SELECT section_id FROM teacher_sections WHERE teacher_id = ? - ) - `).get(activeAyId, userId).count; + WHERE e.section_id IN ( + SELECT section_id FROM teacher_sections WHERE teacher_id = ? + ) + `).get(userId).count; // Count of students in teacher's sections who have ALL mandatory docs complete completedDocuments = db.prepare(` SELECT COUNT(DISTINCT s.id) as count FROM students s JOIN enrollments e ON s.id = e.student_id - WHERE e.academic_year_id = ? - AND e.section_id IN ( - SELECT section_id FROM teacher_sections WHERE teacher_id = ? - ) - AND NOT EXISTS ( - SELECT 1 - FROM document_requirements dr - WHERE dr.is_mandatory = 1 - AND dr.is_enabled = 1 - AND dr.category = (CASE WHEN e.grade_level <= 10 THEN 'JHS' ELSE 'SHS' END) - AND NOT EXISTS ( - SELECT 1 FROM documents d - WHERE d.student_id = s.id - AND (d.requirement_id = dr.id OR d.document_type = dr.name OR d.document_type = dr.category || ' - ' || dr.name) - AND d.status = 'Completed' - AND d.deleted_at IS NULL - ) - ) - `).get(activeAyId, userId).count; + WHERE e.section_id IN ( + SELECT section_id FROM teacher_sections WHERE teacher_id = ? + ) + AND NOT EXISTS ( + SELECT 1 + FROM document_requirements dr + WHERE dr.is_mandatory = 1 + AND dr.is_enabled = 1 + AND dr.category = (CASE WHEN e.grade_level <= 10 THEN 'JHS' ELSE 'SHS' END) + AND NOT EXISTS ( + SELECT 1 FROM documents d + WHERE d.student_id = s.id + AND (d.requirement_id = dr.id OR d.document_type = dr.name OR d.document_type = dr.category || ' - ' || dr.name) + AND d.status = 'Completed' + AND d.deleted_at IS NULL + ) + ) + `).get(userId).count; // Count of students in teacher's assigned sections who are missing at least one mandatory document missingDocuments = db.prepare(` SELECT COUNT(DISTINCT s.id) as count FROM students s JOIN enrollments e ON s.id = e.student_id - WHERE e.academic_year_id = ? - AND e.section_id IN ( - SELECT section_id FROM teacher_sections WHERE teacher_id = ? - ) - AND EXISTS ( - SELECT 1 - FROM document_requirements dr - WHERE dr.is_mandatory = 1 - AND dr.is_enabled = 1 - AND dr.category = (CASE WHEN e.grade_level <= 10 THEN 'JHS' ELSE 'SHS' END) - AND NOT EXISTS ( - SELECT 1 FROM documents d - WHERE d.student_id = s.id - AND (d.requirement_id = dr.id OR d.document_type = dr.name OR d.document_type = dr.category || ' - ' || dr.name) - AND d.status = 'Completed' - AND d.deleted_at IS NULL - ) - ) - `).get(activeAyId, userId).count; + WHERE e.section_id IN ( + SELECT section_id FROM teacher_sections WHERE teacher_id = ? + ) + AND EXISTS ( + SELECT 1 + FROM document_requirements dr + WHERE dr.is_mandatory = 1 + AND dr.is_enabled = 1 + AND dr.category = (CASE WHEN e.grade_level <= 10 THEN 'JHS' ELSE 'SHS' END) + AND NOT EXISTS ( + SELECT 1 FROM documents d + WHERE d.student_id = s.id + AND (d.requirement_id = dr.id OR d.document_type = dr.name OR d.document_type = dr.category || ' - ' || dr.name) + AND d.status = 'Completed' + AND d.deleted_at IS NULL + ) + ) + `).get(userId).count; } else { // Admin: All students across the entire system totalStudents = db.prepare('SELECT COUNT(*) as count FROM students').get().count; @@ -255,31 +252,28 @@ exports.getKpis = (req, res) => { let fileRows, recentRows, olderRows; if (isTeacher) { - // ── 1. Digitalization (Teacher's Students in Active Year) ─────────────────── + // ── 1. Digitalization (Teacher's Students in Assigned Sections) ─────────────────── jhsTotal = db.prepare(` SELECT COUNT(DISTINCT s.id) as count FROM students s JOIN enrollments e ON s.id = e.student_id - WHERE e.academic_year_id = ? - AND e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) + WHERE e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) AND e.grade_level <= 10 - `).get(activeAyId, userId).count; + `).get(userId).count; shsTotal = db.prepare(` SELECT COUNT(DISTINCT s.id) as count FROM students s JOIN enrollments e ON s.id = e.student_id - WHERE e.academic_year_id = ? - AND e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) + WHERE e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) AND e.grade_level > 10 - `).get(activeAyId, userId).count; + `).get(userId).count; jhsDigitized = db.prepare(` SELECT COUNT(DISTINCT s.id) as count FROM students s JOIN enrollments e ON s.id = e.student_id - WHERE e.academic_year_id = ? - AND e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) + WHERE e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) AND e.grade_level <= 10 AND NOT EXISTS ( SELECT 1 @@ -295,14 +289,13 @@ exports.getKpis = (req, res) => { AND d.deleted_at IS NULL ) ) - `).get(activeAyId, userId).count; + `).get(userId).count; shsDigitized = db.prepare(` SELECT COUNT(DISTINCT s.id) as count FROM students s JOIN enrollments e ON s.id = e.student_id - WHERE e.academic_year_id = ? - AND e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) + WHERE e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) AND e.grade_level > 10 AND NOT EXISTS ( SELECT 1 @@ -318,7 +311,7 @@ exports.getKpis = (req, res) => { AND d.deleted_at IS NULL ) ) - `).get(activeAyId, userId).count; + `).get(userId).count; // ── 2. Activity by day (Teacher's Activities / Students) ──────────── activityByDay = db.prepare(` @@ -335,20 +328,20 @@ exports.getKpis = (req, res) => { SELECT s.id FROM students s JOIN enrollments e ON s.id = e.student_id JOIN teacher_sections ts ON e.section_id = ts.section_id - WHERE ts.teacher_id = ? AND e.academic_year_id = ? + WHERE ts.teacher_id = ? )) OR (a.entity_type = 'document' AND a.entity_id IN ( SELECT d.id FROM documents d JOIN enrollments e ON d.student_id = e.student_id JOIN teacher_sections ts ON e.section_id = ts.section_id - WHERE ts.teacher_id = ? AND e.academic_year_id = ? + WHERE ts.teacher_id = ? )) ) GROUP BY day, a.action, a.entity_type ORDER BY day ASC - `).all(userId, userId, activeAyId, userId, activeAyId); + `).all(userId, userId, userId); - // Status distribution will be assigned below from active year requirement counts + // Status distribution will be assigned below from requirement counts let totalCompletedReqsTeacher = 0; let totalArchivedReqsTeacher = 0; @@ -358,23 +351,35 @@ exports.getKpis = (req, res) => { CASE WHEN e.grade_level <= 10 THEN 'JHS' ELSE 'SHS' END as category FROM students s JOIN enrollments e ON s.id = e.student_id - WHERE e.academic_year_id = ? - AND e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) - `).all(activeAyId, userId); + WHERE e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) + `).all(userId); const studentDocStats = enrolledStudents.map(s => { const reqs = s.category === 'JHS' ? jhsReqs : shsReqs; const totalRequired = reqs.length; const reqIds = reqs.map(r => r.id); let uploadedCount = 0; - if (reqIds.length > 0) { - uploadedCount = db.prepare(` - SELECT COUNT(DISTINCT requirement_id) as count - FROM documents - WHERE student_id = ? AND requirement_id IN (${reqIds.join(',')}) - AND status = 'Completed' AND deleted_at IS NULL - `).get(s.id).count; + const completedRows = reqIds.length > 0 ? db.prepare(` + SELECT DISTINCT d.requirement_id, dr.name as req_name + FROM documents d + LEFT JOIN document_requirements dr ON d.requirement_id = dr.id + WHERE d.student_id = ? + AND (d.requirement_id IN (${reqIds.join(',')}) + OR d.document_type IN (${reqs.map(r => `'${r.name.replace(/'/g, "''")}'`).join(',')}) + OR d.document_type IN (${reqs.map(r => `'${s.category} - ${r.name.replace(/'/g, "''")}'`).join(',')})) + AND d.status = 'Completed' AND d.deleted_at IS NULL + `).all(s.id) : []; + + const completedNames = new Set(); + for (const r of completedRows) { + if (r.requirement_id) { + const match = reqs.find(req => req.id === r.requirement_id); + if (match) completedNames.add(match.name); + } + if (r.req_name) completedNames.add(r.req_name); } + uploadedCount = completedNames.size; + const missingReqs = reqs.filter(r => !completedNames.has(r.name)).map(r => r.name); const missingCount = Math.max(0, totalRequired - uploadedCount); return { id: s.id, @@ -384,6 +389,7 @@ exports.getKpis = (req, res) => { uploadedCount, totalRequired, missingCount, + missingRequirements: missingReqs, docCount: uploadedCount, percent: totalRequired === 0 ? 1.0 : uploadedCount / totalRequired }; @@ -401,11 +407,10 @@ exports.getKpis = (req, res) => { SELECT e.grade_level, COUNT(DISTINCT s.id) as total_students FROM students s JOIN enrollments e ON s.id = e.student_id - WHERE e.academic_year_id = ? - AND e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) + WHERE e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) GROUP BY e.grade_level ORDER BY e.grade_level ASC - `).all(activeAyId, userId); + `).all(userId); const gradeTotalsMap = {}; for (const row of gradeTotalsRows) { @@ -430,14 +435,13 @@ exports.getKpis = (req, res) => { JOIN students s ON d.student_id = s.id JOIN enrollments e ON s.id = e.student_id LEFT JOIN document_requirements dr ON d.requirement_id = dr.id - WHERE e.academic_year_id = ? - AND e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) + WHERE e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) AND e.grade_level BETWEEN ? AND ? AND (d.requirement_id = ? OR dr.name = ? OR d.document_type = ? OR d.document_type = ?) AND d.deleted_at IS NULL AND d.status = 'Completed' GROUP BY e.grade_level - `).all(activeAyId, userId, minGrade, maxGrade, req.id, req.name, req.name, `${req.category} - ${req.name}`); + `).all(userId, minGrade, maxGrade, req.id, req.name, req.name, `${req.category} - ${req.name}`); const archCountsByGrade = db.prepare(` SELECT COUNT(DISTINCT d.student_id) as archived_count @@ -445,13 +449,12 @@ exports.getKpis = (req, res) => { JOIN students s ON d.student_id = s.id JOIN enrollments e ON s.id = e.student_id LEFT JOIN document_requirements dr ON d.requirement_id = dr.id - WHERE e.academic_year_id = ? - AND e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) + WHERE e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) AND e.grade_level BETWEEN ? AND ? AND (d.requirement_id = ? OR dr.name = ? OR d.document_type = ? OR d.document_type = ?) AND d.deleted_at IS NULL AND d.status = 'Archived' - `).get(activeAyId, userId, minGrade, maxGrade, req.id, req.name, req.name, `${req.category} - ${req.name}`); + `).get(userId, minGrade, maxGrade, req.id, req.name, req.name, `${req.category} - ${req.name}`); const compSum = countsByGrade.reduce((sum, r) => sum + r.uploaded_count, 0); totalCompletedReqsTeacher += compSum; @@ -479,26 +482,18 @@ exports.getKpis = (req, res) => { }); } - const archivedDocsTeacher = activeAy ? db.prepare(` + const archivedDocsTeacher = db.prepare(` SELECT COUNT(DISTINCT d.id) as total FROM documents d LEFT JOIN students s ON d.student_id = s.id LEFT JOIN enrollments e ON e.student_id = s.id - AND e.id = ( - SELECT e2.id FROM enrollments e2 - JOIN academic_years ay_inner ON e2.academic_year_id = ay_inner.id - WHERE e2.student_id = s.id - ORDER BY ay_inner.year_range DESC, e2.grade_level DESC, e2.id DESC LIMIT 1 - ) JOIN teacher_sections ts ON e.section_id = ts.section_id AND ts.teacher_id = ? - LEFT JOIN academic_years ay ON e.academic_year_id = ay.id WHERE ( (s.status IN ('Graduated','Transferred','Dropped','Inactive') AND d.deleted_at IS NULL) OR (s.status = 'Enrolled' AND d.status = 'Archived' AND d.deleted_at IS NULL) ) - AND ay.year_range = ? - `).get(userId, activeAy.year_range)?.total || 0 : 0; + `).get(userId)?.total || 0; statusDistribution = [ { status: 'Completed', count: totalCompletedReqsTeacher }, @@ -521,13 +516,12 @@ exports.getKpis = (req, res) => { FROM documents d JOIN students s ON d.student_id = s.id JOIN enrollments e ON s.id = e.student_id - WHERE e.academic_year_id = ? - AND e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) + WHERE e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) AND DATE(d.created_at) >= DATE('now', '-29 days') AND d.deleted_at IS NULL GROUP BY day ORDER BY day ASC - `).all(activeAyId, userId); + `).all(userId); // ── 7. Storage analytics ────────────────────────────────────────── fileRows = db.prepare(` @@ -537,31 +531,28 @@ exports.getKpis = (req, res) => { JOIN students s ON d.student_id = s.id JOIN enrollments e ON s.id = e.student_id LEFT JOIN document_requirements dr ON d.requirement_id = dr.id - WHERE e.academic_year_id = ? - AND e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) + WHERE e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) AND d.deleted_at IS NULL - `).all(activeAyId, userId); + `).all(userId); recentRows = db.prepare(` SELECT d.file_path FROM documents d JOIN students s ON d.student_id = s.id JOIN enrollments e ON s.id = e.student_id - WHERE e.academic_year_id = ? - AND e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) + WHERE e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) AND d.deleted_at IS NULL AND d.created_at >= datetime('now', '-30 days') - `).all(activeAyId, userId); + `).all(userId); olderRows = db.prepare(` SELECT d.file_path FROM documents d JOIN students s ON d.student_id = s.id JOIN enrollments e ON s.id = e.student_id - WHERE e.academic_year_id = ? - AND e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) + WHERE e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?) AND d.deleted_at IS NULL AND d.created_at >= datetime('now', '-60 days') AND d.created_at < datetime('now', '-30 days') - `).all(activeAyId, userId); + `).all(userId); } else { // Admin: All students across the school in active academic year // ── 1. Digitalization — JHS/SHS donut ─────────────────────────── @@ -654,14 +645,27 @@ exports.getKpis = (req, res) => { const totalRequired = reqs.length; const reqIds = reqs.map(r => r.id); let uploadedCount = 0; - if (reqIds.length > 0) { - uploadedCount = db.prepare(` - SELECT COUNT(DISTINCT requirement_id) as count - FROM documents - WHERE student_id = ? AND requirement_id IN (${reqIds.join(',')}) - AND status = 'Completed' AND deleted_at IS NULL - `).get(s.id).count; + const completedRows = reqIds.length > 0 ? db.prepare(` + SELECT DISTINCT d.requirement_id, dr.name as req_name + FROM documents d + LEFT JOIN document_requirements dr ON d.requirement_id = dr.id + WHERE d.student_id = ? + AND (d.requirement_id IN (${reqIds.join(',')}) + OR d.document_type IN (${reqs.map(r => `'${r.name.replace(/'/g, "''")}'`).join(',')}) + OR d.document_type IN (${reqs.map(r => `'${s.category} - ${r.name.replace(/'/g, "''")}'`).join(',')})) + AND d.status = 'Completed' AND d.deleted_at IS NULL + `).all(s.id) : []; + + const completedNames = new Set(); + for (const r of completedRows) { + if (r.requirement_id) { + const match = reqs.find(req => req.id === r.requirement_id); + if (match) completedNames.add(match.name); + } + if (r.req_name) completedNames.add(r.req_name); } + uploadedCount = completedNames.size; + const missingReqs = reqs.filter(r => !completedNames.has(r.name)).map(r => r.name); const missingCount = Math.max(0, totalRequired - uploadedCount); return { id: s.id, @@ -671,6 +675,7 @@ exports.getKpis = (req, res) => { uploadedCount, totalRequired, missingCount, + missingRequirements: missingReqs, docCount: uploadedCount, percent: totalRequired === 0 ? 1.0 : uploadedCount / totalRequired }; diff --git a/backend/src/controllers/studentController.js b/backend/src/controllers/studentController.js index ff8da83..f706caa 100644 --- a/backend/src/controllers/studentController.js +++ b/backend/src/controllers/studentController.js @@ -168,16 +168,14 @@ exports.getAllStudents = (req, res) => { conditions.push(`s.is_4ps = 0`); } - // Resolve target school year (defaults to active academic year) - const activeAy = db.prepare("SELECT * FROM academic_years WHERE status = 'active' LIMIT 1").get() - || db.prepare("SELECT * FROM academic_years ORDER BY year_range DESC LIMIT 1").get(); - const effectiveYearRange = (schoolYear && schoolYear.trim()) ? schoolYear.trim() : (activeAy?.year_range || ''); + // Resolve target school year filter (if explicitly passed) + const filterYearRange = (schoolYear && schoolYear.trim()) ? schoolYear.trim() : ''; // grade_level lives in enrollments. // When schoolYear is specified we must join on the enrollment IN that year // (not just the globally-latest enrollment), otherwise students whose newest // enrollment is in a different year are silently excluded. - const needsEnrollmentJoin = gradeLevel.trim() || section.trim() || schoolYear.trim(); + const needsEnrollmentJoin = gradeLevel.trim() || section.trim() || filterYearRange; let enrollmentJoin = ''; const enrollJoinParams = []; // binds consumed by the enrollment JOIN subquery @@ -189,13 +187,13 @@ exports.getAllStudents = (req, res) => { SELECT e.id FROM enrollments e JOIN academic_years ay_inner ON e.academic_year_id = ay_inner.id WHERE e.student_id = s.id - ${effectiveYearRange ? 'AND ay_inner.year_range = ?' : ''} + ${filterYearRange ? 'AND ay_inner.year_range = ?' : ''} ORDER BY ay_inner.year_range DESC, e.grade_level DESC, e.id DESC LIMIT 1 ) JOIN sections sec ON sec.id = e_latest.section_id JOIN academic_years ay ON ay.id = e_latest.academic_year_id`; - if (effectiveYearRange) { - enrollJoinParams.push(effectiveYearRange); + if (filterYearRange) { + enrollJoinParams.push(filterYearRange); } } @@ -209,21 +207,12 @@ exports.getAllStudents = (req, res) => { } // schoolYear is handled inside the JOIN subquery (enrollJoinParams), not WHERE. - // ---- Teacher section scoping (scoped to active school year by default) ---- + // ---- Teacher section scoping ---- const teacherJoin = isTeacher ? `JOIN enrollments e_teacher ON e_teacher.student_id = s.id - AND e_teacher.id = ( - SELECT e.id FROM enrollments e - JOIN academic_years ay_inner ON e.academic_year_id = ay_inner.id - WHERE e.student_id = s.id - ${effectiveYearRange ? 'AND ay_inner.year_range = ?' : ''} - ORDER BY ay_inner.year_range DESC, e.grade_level DESC, e.id DESC LIMIT 1 - ) - JOIN teacher_sections ts ON ts.section_id = e_teacher.section_id AND ts.teacher_id = ?` + AND e_teacher.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?)` : ''; - const teacherJoinParams = isTeacher - ? (effectiveYearRange ? [effectiveYearRange, teacherId] : [teacherId]) - : []; + const teacherJoinParams = isTeacher ? [teacherId] : []; const whereClause = conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''; @@ -262,7 +251,7 @@ exports.getAllStudents = (req, res) => { SELECT e.grade_level FROM enrollments e JOIN academic_years ay_inner ON e.academic_year_id = ay_inner.id WHERE e.student_id = s.id - ${effectiveYearRange ? `AND ay_inner.year_range = '${effectiveYearRange.replace(/'/g, "''")}'` : ''} + ${filterYearRange ? `AND ay_inner.year_range = '${filterYearRange.replace(/'/g, "''")}'` : ''} ORDER BY ay_inner.year_range DESC, e.grade_level DESC, e.id DESC LIMIT 1 ) as latest_grade_level, ( @@ -270,7 +259,7 @@ exports.getAllStudents = (req, res) => { JOIN sections sec ON sec.id = enr.section_id JOIN academic_years ay_inner ON enr.academic_year_id = ay_inner.id WHERE enr.student_id = s.id - ${effectiveYearRange ? `AND ay_inner.year_range = '${effectiveYearRange.replace(/'/g, "''")}'` : ''} + ${filterYearRange ? `AND ay_inner.year_range = '${filterYearRange.replace(/'/g, "''")}'` : ''} ORDER BY ay_inner.year_range DESC, enr.grade_level DESC, enr.id DESC LIMIT 1 ) as latest_section, ( diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 5584af6..85ba7a6 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -431,9 +431,12 @@ Files backup/restore - mirror mode or zip --> # TEACHERS & ACADEMIC SETUP - hide or remove from the list all users have deactivated status --> - + + +# DASHBOARD SCREEN +- "Top students by documents" and "needs attention" view all -> list of students count of missing documents when hover shows missing requirements separate jhs shs --- diff --git a/frontend/lib/domain/entities/dashboard_models.dart b/frontend/lib/domain/entities/dashboard_models.dart index 2f1fb4c..a640bd0 100644 --- a/frontend/lib/domain/entities/dashboard_models.dart +++ b/frontend/lib/domain/entities/dashboard_models.dart @@ -231,6 +231,7 @@ class StudentDocCount { final int uploadedCount; final int totalRequired; final int missingCount; + final List missingRequirements; final double percent; final int? gradeLevel; final String category; @@ -242,6 +243,7 @@ class StudentDocCount { this.uploadedCount = 0, this.totalRequired = 0, this.missingCount = 0, + this.missingRequirements = const [], this.percent = 0.0, this.gradeLevel, this.category = 'JHS', @@ -256,6 +258,11 @@ class StudentDocCount { final pct = (j['percent'] as num?)?.toDouble() ?? (total == 0 ? (uploaded > 0 ? 1.0 : 0.0) : uploaded / total); + final missingReqs = (j['missingRequirements'] as List?) + ?.map((e) => e.toString()) + .toList() ?? + const []; + return StudentDocCount( id: (j['id'] as num?)?.toInt() ?? 0, name: j['name'] as String? ?? '', @@ -263,6 +270,7 @@ class StudentDocCount { uploadedCount: uploaded, totalRequired: total, missingCount: missing, + missingRequirements: missingReqs, percent: pct, gradeLevel: (j['gradeLevel'] as num?)?.toInt(), category: j['category'] as String? ?? 'JHS', diff --git a/frontend/lib/ui/screens/dashboard/widgets/dashboard_kpis.dart b/frontend/lib/ui/screens/dashboard/widgets/dashboard_kpis.dart index b59bf18..d9366b3 100644 --- a/frontend/lib/ui/screens/dashboard/widgets/dashboard_kpis.dart +++ b/frontend/lib/ui/screens/dashboard/widgets/dashboard_kpis.dart @@ -1633,16 +1633,49 @@ class _StudentDocCard extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), - Text( - '${s.uploadedCount} / ${s.totalRequired} (${s.missingCount} missing)', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: s.missingCount == 0 - ? (isDark ? const Color(0xFF66BB6A) : AppColors.primaryGreen) - : (isTop - ? Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7) - : Colors.orange.shade700), + Tooltip( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1E293B) : const Color(0xFF0F172A), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + textStyle: const TextStyle(fontSize: 11, color: Colors.white, height: 1.4), + message: s.missingCount == 0 + ? '[${s.category}] All ${s.totalRequired} required documents submitted! ✅' + : '[${s.category} Missing Requirements (${s.missingCount})]:\n• ${s.missingRequirements.join('\n• ')}', + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${s.uploadedCount} / ${s.totalRequired} (${s.missingCount} missing)', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: s.missingCount == 0 + ? (isDark ? const Color(0xFF66BB6A) : AppColors.primaryGreen) + : (isTop + ? Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7) + : Colors.orange.shade700), + ), + ), + if (s.missingCount > 0) ...[ + const SizedBox(width: 4), + Icon( + Icons.info_outline_rounded, + size: 12, + color: s.missingCount == 0 + ? AppColors.primaryGreen + : Colors.orange.shade700, + ), + ], + ], ), ), ], @@ -2060,23 +2093,54 @@ class _StudentDocRankingModalState extends State<_StudentDocRankingModal> { ), const SizedBox(width: 10), - // Status Pill - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + // Status Pill with hover tooltip showing missing requirements separated by category + Tooltip( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: isComplete - ? AppColors.primaryGreen.withValues(alpha: isDark ? 0.25 : 0.12) - : Colors.orange.withValues(alpha: isDark ? 0.25 : 0.12), - borderRadius: BorderRadius.circular(4), + color: isDark ? const Color(0xFF1E293B) : const Color(0xFF0F172A), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], ), - child: Text( - isComplete ? 'Complete' : '${s.missingCount} Missing', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, + textStyle: const TextStyle(fontSize: 11, color: Colors.white, height: 1.4), + message: isComplete + ? '[${s.category}] All ${s.totalRequired} required documents submitted! ✅' + : '[${s.category} Missing Requirements (${s.missingCount})]:\n• ${s.missingRequirements.join('\n• ')}', + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( color: isComplete - ? (isDark ? const Color(0xFF66BB6A) : AppColors.primaryGreen) - : (isDark ? Colors.orange.shade300 : Colors.orange.shade800), + ? AppColors.primaryGreen.withValues(alpha: isDark ? 0.25 : 0.12) + : Colors.orange.withValues(alpha: isDark ? 0.25 : 0.12), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + isComplete ? 'Complete' : '${s.missingCount} Missing', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: isComplete + ? (isDark ? const Color(0xFF66BB6A) : AppColors.primaryGreen) + : (isDark ? Colors.orange.shade300 : Colors.orange.shade800), + ), + ), + if (!isComplete) ...[ + const SizedBox(width: 4), + Icon( + Icons.info_outline_rounded, + size: 11, + color: isDark ? Colors.orange.shade300 : Colors.orange.shade800, + ), + ], + ], ), ), ),