1- // const mongoose = require('mongoose');
1+ const mongoose = require ( 'mongoose' ) ;
22const Progress = require ( '../models/progress' ) ;
33const UserProfile = require ( '../models/userProfile' ) ;
44const Atom = require ( '../models/atom' ) ;
5+ const EducationStudentProfile = require ( '../models/educationStudentProfile' ) ;
6+
7+ const normalizeToArray = ( value ) => {
8+ if ( Array . isArray ( value ) ) {
9+ return value ;
10+ }
11+
12+ if ( value === null || value === undefined ) {
13+ return [ ] ;
14+ }
15+
16+ return [ value ] ;
17+ } ;
18+
19+ const buildManualStudentProgressResponse = ( profile ) => {
20+ const completedAtoms = [ ] ;
21+ const inProgressAtoms = [ ] ;
22+ const notStartedAtoms = [ ] ;
23+
24+ const subjects = Array . isArray ( profile . subjects ) ? profile . subjects : [ ] ;
25+
26+ subjects . forEach ( ( subject ) => {
27+ const subjectName = subject ?. name || 'General' ;
28+ const molecules = Array . isArray ( subject ?. molecules ) ? subject . molecules : [ ] ;
29+
30+ molecules . forEach ( ( molecule , index ) => {
31+ const status = molecule ?. status || 'not_started' ;
32+ const fallbackId = `${ subjectName } -${ molecule ?. label || molecule ?. name || index } `
33+ . toLowerCase ( )
34+ . replace ( / [ ^ a - z 0 - 9 ] + / g, '-' ) ;
35+
36+ const atomData = {
37+ atomId : molecule ?. atomId || fallbackId ,
38+ name : molecule ?. label || molecule ?. name || `Molecule ${ index + 1 } ` ,
39+ description : molecule ?. description || '' ,
40+ difficulty : molecule ?. difficulty || 'medium' ,
41+ moleculeType : molecule ?. moleculeType || subjectName ,
42+ subject : subjectName ,
43+ status,
44+ grade : molecule ?. grade || ( status === 'completed' ? 'A' : 'pending' ) ,
45+ timestamp :
46+ molecule ?. completedAt ||
47+ molecule ?. startedAt ||
48+ profile ?. updatedAt ||
49+ profile ?. createdAt ||
50+ null ,
51+ sourceTask : molecule ?. sourceTask
52+ ? {
53+ reference : molecule . sourceTask ,
54+ taskType : molecule ?. taskType || null ,
55+ lessonPlan : molecule ?. lessonPlan || null ,
56+ assignedAt : molecule ?. assignedAt || null ,
57+ completedAt : molecule ?. completedAt || null ,
58+ }
59+ : null ,
60+ } ;
61+
62+ if ( status === 'completed' ) {
63+ completedAtoms . push ( atomData ) ;
64+ } else if ( status === 'in_progress' ) {
65+ inProgressAtoms . push ( atomData ) ;
66+ } else {
67+ notStartedAtoms . push ( atomData ) ;
68+ }
69+ } ) ;
70+ } ) ;
71+
72+ const summaryTotals = profile ?. progressSummary || { } ;
73+ const totalAtomsFallback =
74+ completedAtoms . length + inProgressAtoms . length + notStartedAtoms . length ;
75+
76+ return {
77+ student : {
78+ id : profile . _id ,
79+ firstName : profile ?. firstName || ( profile ?. name ? profile . name . split ( ' ' ) [ 0 ] : 'Student' ) ,
80+ lastName :
81+ profile ?. lastName || ( profile ?. name ? profile . name . split ( ' ' ) . slice ( 1 ) . join ( ' ' ) : '' ) ,
82+ email : profile ?. email || '' ,
83+ profilePic : profile ?. avatarUrl || null ,
84+ location : profile ?. location || '' ,
85+ educationProfile : {
86+ learningLevel : profile ?. gradeLevel || profile ?. learningLevel || null ,
87+ strengths : normalizeToArray (
88+ profile ?. strengths ||
89+ profile ?. educationProfile ?. student ?. strengths ||
90+ profile ?. student ?. strengths ,
91+ ) ,
92+ challengingAreas : normalizeToArray (
93+ profile ?. challengingAreas ||
94+ profile ?. educationProfile ?. student ?. challengingAreas ||
95+ profile ?. student ?. challengingAreas ,
96+ ) ,
97+ } ,
98+ } ,
99+ progress : {
100+ completed : completedAtoms ,
101+ inProgress : inProgressAtoms ,
102+ notStarted : notStartedAtoms ,
103+ } ,
104+ summary : {
105+ totalCompleted : summaryTotals . totalCompleted ?? completedAtoms . length ,
106+ totalInProgress : summaryTotals . totalInProgress ?? inProgressAtoms . length ,
107+ totalNotStarted : summaryTotals . totalNotStarted ?? notStartedAtoms . length ,
108+ totalAtoms : summaryTotals . totalAtoms ?? totalAtomsFallback ,
109+ } ,
110+ } ;
111+ } ;
5112
6113const progressController = function ( ) {
7114 // Get all progress records
@@ -86,29 +193,33 @@ const progressController = function () {
86193 try {
87194 const { studentId, atomId, status, grade, feedback } = req . body ;
88195
196+ // Sanitize inputs to prevent NoSQL injection
197+ const sanitizedStudentId = String ( studentId ) ;
198+ const sanitizedAtomId = String ( atomId ) ;
199+
89200 // Validate student exists
90- const student = await UserProfile . findById ( studentId ) ;
201+ const student = await UserProfile . findById ( sanitizedStudentId ) ;
91202 if ( ! student ) {
92203 return res . status ( 404 ) . json ( { error : 'Student not found' } ) ;
93204 }
94205
95206 // Validate atom exists
96- const atom = await Atom . findById ( atomId ) ;
207+ const atom = await Atom . findById ( sanitizedAtomId ) ;
97208 if ( ! atom ) {
98209 return res . status ( 404 ) . json ( { error : 'Atom not found' } ) ;
99210 }
100211
101212 // Check if progress record already exists
102- const existingProgress = await Progress . findOne ( { studentId, atomId } ) ;
213+ const existingProgress = await Progress . findOne ( { studentId : sanitizedStudentId , atomId : sanitizedAtomId } ) ;
103214 if ( existingProgress ) {
104215 return res
105216 . status ( 400 )
106217 . json ( { error : 'Progress record already exists for this student and atom' } ) ;
107218 }
108219
109220 const progress = new Progress ( {
110- studentId,
111- atomId,
221+ studentId : sanitizedStudentId ,
222+ atomId : sanitizedAtomId ,
112223 status : status || 'not_started' ,
113224 grade : grade || 'pending' ,
114225 feedback,
@@ -324,6 +435,156 @@ const progressController = function () {
324435 }
325436 } ;
326437
438+ // Get educator view of student progress with molecules (atoms)
439+ const getEducatorStudentProgress = async ( req , res ) => {
440+ try {
441+ const { studentId } = req . params ;
442+
443+ let manualProfile = null ;
444+ if ( mongoose . Types . ObjectId . isValid ( studentId ) ) {
445+ manualProfile = await EducationStudentProfile . findById ( studentId ) . lean ( ) ;
446+ }
447+
448+ if ( manualProfile ) {
449+ const response = buildManualStudentProgressResponse ( manualProfile ) ;
450+ return res . status ( 200 ) . json ( response ) ;
451+ }
452+
453+ const EducationTask = require ( '../models/educationTask' ) ;
454+
455+ // Validate student exists
456+ const student = await UserProfile . findById ( studentId ) . select (
457+ 'firstName lastName email educationProfiles profilePic location' ,
458+ ) ;
459+
460+ if ( ! student ) {
461+ return res . status ( 404 ) . json ( { error : 'Student not found' } ) ;
462+ }
463+
464+ // Get all atoms and their progress for this student
465+ const progressRecords = await Progress . find ( { studentId } )
466+ . populate ( {
467+ path : 'atomId' ,
468+ select : 'name description difficulty subjectId moleculeType' ,
469+ populate : {
470+ path : 'subjectId' ,
471+ select : 'name' ,
472+ } ,
473+ } )
474+ . sort ( { updatedAt : - 1 } ) ;
475+
476+ // Get all tasks for this student to find source task info
477+ const tasks = await EducationTask . find ( { studentId } )
478+ . populate ( 'lessonPlanId' , 'title theme' )
479+ . populate ( 'atomIds' , 'name' ) ;
480+
481+ // Create a map of atomId to task info
482+ const atomToTaskMap = { } ;
483+ tasks . forEach ( ( task ) => {
484+ if ( task . atomIds && task . atomIds . length > 0 ) {
485+ task . atomIds . forEach ( ( atom ) => {
486+ if ( ! atomToTaskMap [ atom . _id ] ) {
487+ atomToTaskMap [ atom . _id ] = {
488+ taskId : task . _id ,
489+ taskType : task . type ,
490+ lessonPlan : task . lessonPlanId ? task . lessonPlanId . title : null ,
491+ assignedAt : task . assignedAt ,
492+ completedAt : task . completedAt ,
493+ } ;
494+ }
495+ } ) ;
496+ }
497+ } ) ;
498+
499+ // Categorize atoms by status
500+ const completedAtoms = [ ] ;
501+ const inProgressAtoms = [ ] ;
502+ const notStartedAtoms = [ ] ;
503+
504+ progressRecords . forEach ( ( progress ) => {
505+ if ( ! progress . atomId ) return ;
506+
507+ const atomData = {
508+ atomId : progress . atomId . _id ,
509+ name : progress . atomId . name ,
510+ description : progress . atomId . description ,
511+ difficulty : progress . atomId . difficulty ,
512+ moleculeType : progress . atomId . moleculeType ,
513+ subject : progress . atomId . subjectId ? progress . atomId . subjectId . name : null ,
514+ status : progress . status ,
515+ grade : progress . grade ,
516+ timestamp : progress . updatedAt ,
517+ sourceTask : atomToTaskMap [ progress . atomId . _id ] || null ,
518+ } ;
519+
520+ if ( progress . status === 'completed' ) {
521+ completedAtoms . push ( atomData ) ;
522+ } else if ( progress . status === 'in_progress' ) {
523+ inProgressAtoms . push ( atomData ) ;
524+ } else {
525+ notStartedAtoms . push ( atomData ) ;
526+ }
527+ } ) ;
528+
529+ // Get all atoms to show unearned ones
530+ const allAtoms = await Atom . find ( { } )
531+ . populate ( 'subjectId' , 'name' )
532+ . select ( 'name description difficulty moleculeType subjectId' ) ;
533+
534+ // Find unearned atoms (atoms not in progress records)
535+ const progressAtomIds = new Set ( progressRecords . map ( ( p ) => p . atomId ?. _id . toString ( ) ) . filter ( Boolean ) ) ;
536+ const unearnedAtoms = allAtoms
537+ . filter ( ( atom ) => ! progressAtomIds . has ( atom . _id . toString ( ) ) )
538+ . map ( ( atom ) => ( {
539+ atomId : atom . _id ,
540+ name : atom . name ,
541+ description : atom . description ,
542+ difficulty : atom . difficulty ,
543+ moleculeType : atom . moleculeType ,
544+ subject : atom . subjectId ? atom . subjectId . name : null ,
545+ status : 'not_started' ,
546+ grade : 'pending' ,
547+ timestamp : null ,
548+ sourceTask : null ,
549+ } ) ) ;
550+
551+ const locationParts = [ ] ;
552+ if ( student ?. location ?. city ) {
553+ locationParts . push ( student . location . city ) ;
554+ }
555+ if ( student ?. location ?. country ) {
556+ locationParts . push ( student . location . country ) ;
557+ }
558+
559+ const response = {
560+ student : {
561+ id : student . _id ,
562+ firstName : student . firstName ,
563+ lastName : student . lastName ,
564+ email : student . email ,
565+ profilePic : student . profilePic || null ,
566+ location : locationParts . join ( ', ' ) ,
567+ educationProfile : student . educationProfiles ?. student || null ,
568+ } ,
569+ progress : {
570+ completed : completedAtoms ,
571+ inProgress : inProgressAtoms ,
572+ notStarted : notStartedAtoms . concat ( unearnedAtoms ) ,
573+ } ,
574+ summary : {
575+ totalCompleted : completedAtoms . length ,
576+ totalInProgress : inProgressAtoms . length ,
577+ totalNotStarted : notStartedAtoms . length + unearnedAtoms . length ,
578+ totalAtoms : allAtoms . length ,
579+ } ,
580+ } ;
581+
582+ res . status ( 200 ) . json ( response ) ;
583+ } catch ( error ) {
584+ res . status ( 500 ) . json ( { error : error . message } ) ;
585+ }
586+ } ;
587+
327588 return {
328589 getProgress,
329590 getProgressByStudent,
@@ -337,6 +598,7 @@ const progressController = function () {
337598 gradeProgress,
338599 getProgressByStatus,
339600 getStudentProgressSummary,
601+ getEducatorStudentProgress,
340602 } ;
341603} ;
342604
0 commit comments