tarekdx3 commited on
Commit
1af4e1f
·
1 Parent(s): 8c85b71

Labs - Quizzes - Assignment student Dashboard still under work

Browse files
COMPLETE_API_ENDPOINTS.md DELETED
@@ -1,317 +0,0 @@
1
- # 📊 EduVerse Backend - Complete API Endpoints Summary
2
-
3
- ## AUTH MODULE - api/auth (Public + Protected)
4
- POST /api/auth/register - Register new user
5
- POST /api/auth/login - User login
6
- POST /api/auth/logout - Logout (JWT required)
7
- POST /api/auth/refresh-token - Refresh access token
8
- POST /api/auth/forgot-password - Request password reset
9
- POST /api/auth/reset-password - Reset with token
10
- GET /api/auth/me - Get current user profile
11
- GET /api/admin/users - List users (ADMIN)
12
- POST /api/admin/users - Create user (ADMIN)
13
- GET /api/admin/users/:id - Get user details (ADMIN)
14
- PATCH /api/admin/users/:id - Update user (ADMIN)
15
- DELETE /api/admin/users/:id - Delete user (ADMIN)
16
- POST /api/admin/users/:id/roles - Assign roles (ADMIN)
17
-
18
- ## COURSES MODULE - api/courses
19
- GET /api/courses - List all courses
20
- GET /api/courses/department/:deptId - Courses by department
21
- GET /api/courses/:id - Get course details
22
- POST /api/courses - Create course (INSTRUCTOR/ADMIN)
23
- PATCH /api/courses/:id - Update course
24
- DELETE /api/courses/:id - Delete course
25
- GET /api/sections - List sections
26
- POST /api/sections - Create section
27
- GET /api/sections/:id - Get section details
28
- PATCH /api/sections/:id - Update section
29
- DELETE /api/sections/:id - Delete section
30
- GET /api/schedules - List schedules
31
- POST /api/schedules - Create schedule
32
- GET /api/schedules/:id - Get schedule
33
-
34
- ## ENROLLMENTS MODULE - api/enrollments (JWT + Role-based)
35
- GET /api/enrollments/my-courses - Get student's enrolled courses (STUDENT)
36
- GET /api/enrollments/available - Get available courses to enroll (STUDENT)
37
- POST /api/enrollments - Enroll in course (STUDENT)
38
- DELETE /api/enrollments/:enrollmentId - Drop course (STUDENT)
39
- GET /api/enrollments/:courseId - List course enrollments (INSTRUCTOR/ADMIN)
40
- PATCH /api/enrollments/:id - Update enrollment (ADMIN)
41
-
42
- ## ASSIGNMENTS MODULE - api/assignments (JWT + Role-based)
43
- GET /api/assignments - List assignments (paginated, filtered)
44
- POST /api/assignments - Create assignment (INSTRUCTOR/ADMIN)
45
- GET /api/assignments/:id - Get assignment with submissions
46
- PATCH /api/assignments/:id - Update assignment
47
- DELETE /api/assignments/:id - Delete assignment
48
- POST /api/assignments/:id/submit - Submit assignment (STUDENT)
49
- POST /api/assignments/:submissionId/grade - Grade submission (INSTRUCTOR/ADMIN)
50
- GET /api/assignments/:id/submissions - List all submissions
51
-
52
- ## QUIZZES MODULE - quizzes (JWT + Role-based)
53
- POST /quizzes - Create quiz (INSTRUCTOR/TA/ADMIN)
54
- GET /quizzes - List quizzes
55
- GET /quizzes/:quizId - Get quiz details
56
- PATCH /quizzes/:quizId - Update quiz
57
- DELETE /quizzes/:quizId - Delete quiz
58
- POST /quizzes/:quizId/start - Start quiz attempt (STUDENT)
59
- POST /quizzes/:quizId/submit - Submit quiz answers
60
- GET /quizzes/:quizId/results/:attemptId - Get attempt results
61
- GET /quizzes/:quizId/statistics - Quiz statistics (INSTRUCTOR)
62
- POST /quizzes/:quizId/questions - Add question
63
- PATCH /quizzes/:quizId/questions/:qId - Update question
64
- DELETE /quizzes/:quizId/questions/:qId - Delete question
65
-
66
- ## GRADES MODULE - api/grades (JWT + Role-based)
67
- GET /api/grades - List grades (INSTRUCTOR/TA/ADMIN)
68
- GET /api/grades/my - Get my grades (STUDENT)
69
- GET /api/grades/transcript/:studentId - Get transcript
70
- GET /api/grades/gpa/:studentId - Calculate GPA
71
- GET /api/grades/distribution/:courseId - Grade distribution
72
- PUT /api/grades/:id - Update grade
73
- POST /api/grades - Create grade (INSTRUCTOR/ADMIN)
74
- GET /api/rubrics - List rubrics
75
- POST /api/rubrics - Create rubric
76
- GET /api/rubrics/:id - Get rubric
77
- PATCH /api/rubrics/:id - Update rubric
78
- DELETE /api/rubrics/:id - Delete rubric
79
-
80
- ## ATTENDANCE MODULE - attendance (JWT + Role-based)
81
- POST /attendance/sessions - Create session (INSTRUCTOR)
82
- PATCH /attendance/sessions/:id - Update session
83
- GET /attendance/sessions/:sessionId - Get session records
84
- POST /attendance/mark - Mark attendance (INSTRUCTOR/SYSTEM)
85
- POST /attendance/batch - Batch mark attendance
86
- GET /attendance/summary/:courseId - Attendance summary
87
- POST /attendance/import - Import from Excel
88
- GET /attendance/student/:studentId - Student attendance
89
-
90
- ## LABS MODULE - api/labs (JWT + Role-based)
91
- GET /api/labs - List labs
92
- POST /api/labs - Create lab (INSTRUCTOR/ADMIN)
93
- GET /api/labs/:id - Get lab details
94
- PATCH /api/labs/:id - Update lab
95
- DELETE /api/labs/:id - Delete lab
96
- POST /api/labs/:id/submit - Submit lab (STUDENT)
97
- GET /api/labs/:id/submissions - List submissions
98
- POST /api/labs/:submissionId/grade - Grade submission
99
-
100
- ## ANNOUNCEMENTS MODULE - api/announcements (JWT + Role-based)
101
- GET /api/announcements - List announcements
102
- POST /api/announcements - Create announcement (INSTRUCTOR/ADMIN)
103
- GET /api/announcements/:id - Get announcement
104
- PATCH /api/announcements/:id - Update announcement
105
- DELETE /api/announcements/:id - Delete announcement
106
- POST /api/announcements/:id/schedule - Schedule announcement
107
-
108
- ## DISCUSSIONS MODULE - api/discussions (JWT + Role-based)
109
- GET /api/discussions - List discussions/threads
110
- POST /api/discussions - Create thread
111
- GET /api/discussions/:threadId - Get thread with messages
112
- POST /api/discussions/:threadId/message - Post message
113
- DELETE /api/discussions/:messageId - Delete message
114
-
115
- ## COMMUNITY MODULE - api/community* (JWT + Role-based)
116
- GET /api/community - List communities
117
- POST /api/community - Create community
118
- GET /api/community/:id - Get community
119
- PATCH /api/community/:id - Update community
120
- DELETE /api/community/:id - Delete community
121
- GET /api/community/posts - List posts (all)
122
- POST /api/community/posts - Create post
123
- GET /api/community/posts/:postId - Get post with comments
124
- PATCH /api/community/posts/:postId - Update post
125
- DELETE /api/community/posts/:postId - Delete post
126
- POST /api/community/posts/:postId/upvote - Upvote post
127
- GET /api/community/comments - List comments
128
- POST /api/community/comments - Create comment
129
- DELETE /api/community/comments/:id - Delete comment
130
- POST /api/community/reactions - React to post/comment
131
- GET /api/community/categories - List categories
132
- POST /api/community/categories - Create category
133
- PATCH /api/community/categories/:id - Update category
134
- DELETE /api/community/categories/:id - Delete category
135
-
136
- ## MESSAGING MODULE - api/messages (JWT + Role-based)
137
- GET /api/messages - List conversations
138
- POST /api/messages - Send message
139
- GET /api/messages/:conversationId - Get conversation
140
- POST /api/messages/:messageId/reply - Reply to message
141
- GET /api/messages/search - Search messages
142
- DELETE /api/messages/:messageId - Delete message
143
-
144
- ## NOTIFICATIONS MODULE - api/notifications (JWT)
145
- GET /api/notifications - Get user notifications
146
- POST /api/notifications/:id/read - Mark as read
147
- GET /api/notifications/preferences - Get preferences
148
- PATCH /api/notifications/preferences - Update preferences
149
- DELETE /api/notifications/:id - Delete notification
150
-
151
- ## SCHEDULE MODULE - api/schedule, api/exams/schedule, api/calendar/* (JWT + Role-based)
152
- GET /api/schedule - Get user schedule
153
- GET /api/exams/schedule - List exam schedules
154
- POST /api/exams/schedule - Create exam schedule (INSTRUCTOR/ADMIN)
155
- PATCH /api/exams/schedule/:id - Update exam schedule
156
- DELETE /api/exams/schedule/:id - Delete exam schedule
157
- GET /api/calendar/events - List calendar events
158
- POST /api/calendar/events - Create event
159
- PATCH /api/calendar/events/:id - Update event
160
- DELETE /api/calendar/events/:id - Delete event
161
- POST /api/calendar/integrations - Connect calendar (Google/Outlook)
162
- GET /api/calendar/integrations - Get integration status
163
-
164
- ## COURSE MATERIALS MODULE - api/courses/:courseId/materials* (JWT + Role-based)
165
- GET /api/courses/:courseId/materials - List materials
166
- POST /api/courses/:courseId/materials - Upload material (INSTRUCTOR)
167
- GET /api/courses/:courseId/materials/:id - Get material
168
- PATCH /api/courses/:courseId/materials/:id - Update material
169
- DELETE /api/courses/:courseId/materials/:id - Delete material
170
- POST /api/courses/:courseId/materials/bulk - Bulk upload
171
- GET /api/courses/:courseId/structure - Get course structure
172
- POST /api/courses/:courseId/structure - Create structure
173
- PATCH /api/courses/:courseId/structure/:id - Update structure
174
- POST /api/courses/:courseId/structure/reorder - Reorder modules
175
-
176
- ## TASKS MODULE - api/tasks (JWT + Role-based)
177
- GET /api/tasks - List tasks (STUDENT/INSTRUCTOR)
178
- POST /api/tasks - Create task
179
- GET /api/tasks/:id - Get task
180
- PATCH /api/tasks/:id - Update task
181
- DELETE /api/tasks/:id - Delete task
182
- POST /api/tasks/:id/complete - Mark complete
183
- GET /api/tasks/reminders - Get deadline reminders
184
- POST /api/tasks/:id/remind - Create reminder
185
-
186
- ## REPORTS MODULE - api/reports (JWT + Role-based)
187
- GET /api/reports - List reports
188
- POST /api/reports/generate - Generate report (INSTRUCTOR/ADMIN)
189
- GET /api/reports/:id - Get report
190
- DELETE /api/reports/:id - Delete report
191
- POST /api/reports/:id/export - Export report
192
-
193
- ## ANALYTICS MODULE - api/analytics (JWT + Role-based)
194
- GET /api/analytics/courses/:courseId - Course analytics (INSTRUCTOR/ADMIN)
195
- GET /api/analytics/students/:studentId - Student analytics (STUDENT/ADMIN)
196
- GET /api/analytics/class/:courseId - Class overview analytics
197
- GET /api/analytics/dashboard - Dashboard data
198
-
199
- ## SEARCH MODULE - api/search (JWT)
200
- GET /api/search - Global search (courses, materials, announcements)
201
- GET /api/search/history - Get search history
202
- DELETE /api/search/history/:id - Delete history entry
203
- GET /api/search/advanced - Advanced search with filters
204
-
205
- ## CAMPUS MODULE - api/* (JWT + Role-based)
206
- GET /api/campuses - List campuses
207
- POST /api/campuses - Create campus (ADMIN)
208
- GET /api/campuses/:id - Get campus
209
- PATCH /api/campuses/:id - Update campus
210
- DELETE /api/campuses/:id - Delete campus
211
- GET /api/departments - List departments
212
- POST /api/departments - Create department (ADMIN)
213
- GET /api/departments/:id - Get department
214
- PATCH /api/departments/:id - Update department
215
- DELETE /api/departments/:id - Delete department
216
- GET /api/programs - List programs
217
- POST /api/programs - Create program (ADMIN)
218
- GET /api/semesters - List semesters
219
- POST /api/semesters - Create semester (ADMIN)
220
- PATCH /api/semesters/:id - Update semester
221
- DELETE /api/semesters/:id - Delete semester
222
-
223
- ## FILES MODULE - api/files* (JWT + Role-based)
224
- GET /api/files - List files
225
- POST /api/files - Upload file
226
- GET /api/files/:fileId - Download file
227
- DELETE /api/files/:fileId - Delete file
228
- GET /api/files/:fileId/versions - Get file versions
229
- POST /api/files/:fileId/share - Share file
230
- DELETE /api/files/:fileId/permissions/:userId - Revoke permission
231
- GET /api/files/folders - List folders
232
- POST /api/files/folders - Create folder
233
- GET /api/files/folders/:folderId - Get folder contents
234
- PATCH /api/files/folders/:folderId - Update folder
235
- DELETE /api/files/folders/:folderId - Delete folder
236
-
237
- ## YOUTUBE MODULE - youtube (JWT + Role-based)
238
- POST /youtube/upload - Upload video to YouTube
239
- GET /youtube/status/:videoId - Get upload status
240
-
241
- ---
242
-
243
- ## Authentication & Authorization
244
-
245
- ### Public Endpoints (No JWT Required):
246
- - POST /api/auth/register
247
- - POST /api/auth/login
248
- - POST /api/auth/refresh-token
249
- - POST /api/auth/forgot-password
250
- - POST /api/auth/reset-password
251
- - GET /api/courses (list only)
252
-
253
- ### Protected Endpoints (JWT Required):
254
- - All endpoints requiring @UseGuards(JwtAuthGuard)
255
- - Role-based access via @Roles() decorator
256
-
257
- ### Role-Based Access Control:
258
- - **STUDENT**: Can access own courses, submissions, grades
259
- - **INSTRUCTOR**: Can create courses, assignments, quizzes; grade submissions
260
- - **TA**: Can manage assignments/quizzes; grade under instructor
261
- - **ADMIN**: Full access to all resources
262
- - **IT_ADMIN**: System administration
263
-
264
- ### Decorators Used:
265
- - @UseGuards(JwtAuthGuard, RolesGuard)
266
- - @Roles(RoleName.STUDENT, RoleName.INSTRUCTOR, etc.)
267
- - @ApiBearerAuth('JWT-auth') - Swagger documentation
268
- - @Public() - Marks public endpoints
269
-
270
- ---
271
-
272
- ## Common Query Parameters
273
-
274
- ### Pagination:
275
- - **page** - Page number (default: 1)
276
- - **limit** - Items per page (default: 20, max: 100)
277
-
278
- ### Filtering (varies by endpoint):
279
- - **status** - Filter by status enum
280
- - **departmentId** - Filter by department
281
- - **courseId** - Filter by course
282
- - **search** - Full-text search
283
- - **semester** - Filter by semester
284
- - **date range** - startDate, endDate
285
-
286
- ### Sorting:
287
- - **sort** - Field to sort by
288
- - **order** - ASC or DESC
289
-
290
- ---
291
-
292
- ## Response Format Standards
293
-
294
- ### Success Response (2xx):
295
- \\\json
296
- {
297
- "data": {...} or [...],
298
- "message": "Success message",
299
- "timestamp": "2024-01-15T10:30:00Z"
300
- }
301
- \\\
302
-
303
- ### Error Response (4xx/5xx):
304
- \\\json
305
- {
306
- "statusCode": 400,
307
- "message": "Error description",
308
- "error": "BadRequest | Unauthorized | Forbidden | NotFound",
309
- "timestamp": "2024-01-15T10:30:00Z"
310
- }
311
- \\\
312
-
313
- ---
314
-
315
- ## File Saved
316
- ✅ Full module analysis: COMPLETE_MODULE_ANALYSIS.md
317
- ✅ API Endpoints summary: COMPLETE_API_ENDPOINTS.md
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
COMPLETE_MODULE_ANALYSIS.md DELETED
@@ -1,954 +0,0 @@
1
- # 🏗️ EduVerse Backend - Complete NestJS Module Structure Analysis
2
-
3
- ## Overview
4
- **Total Modules:** 23
5
- **Architecture:** Modular NestJS with TypeORM
6
- **Database:** MySQL
7
- **All modules registered in:** src/app.module.ts
8
-
9
- ---
10
-
11
- ## 📋 Table of Contents
12
- 1. [Module Registration](#module-registration)
13
- 2. [Core Modules](#core-modules)
14
- 3. [Academic Modules](#academic-modules)
15
- 4. [Community & Discussion Modules](#community--discussion-modules)
16
- 5. [Administrative Modules](#administrative-modules)
17
- 6. [Infrastructure Modules](#infrastructure-modules)
18
-
19
- ---
20
-
21
- ## Module Registration
22
-
23
- ### All 23 modules imported in src/app.module.ts:
24
- - AuthModule
25
- - EmailModule
26
- - CampusModule
27
- - CoursesModule
28
- - EnrollmentsModule
29
- - YoutubeModule
30
- - FilesModule
31
- - AssignmentsModule
32
- - GradesModule
33
- - AttendanceModule
34
- - QuizzesModule
35
- - LabsModule
36
- - NotificationsModule
37
- - MessagingModule
38
- - DiscussionsModule
39
- - AnnouncementsModule
40
- - CommunityModule
41
- - ScheduleModule
42
- - CourseMaterialsModule
43
- - TasksModule
44
- - ReportsModule
45
- - SearchModule
46
- - AnalyticsModule
47
-
48
- ---
49
-
50
- ## Core Modules
51
-
52
- ### 1️⃣ AUTH MODULE (Authentication & Authorization)
53
- **Location:** \src/modules/auth/\
54
-
55
- #### Entities (8):
56
- - **User** - Primary user entity with roles, sessions
57
- - Fields: userId, email, passwordHash, firstName, lastName, phone, profilePictureUrl, bio, socialLinks, campusId, status (ACTIVE/INACTIVE/SUSPENDED/PENDING), emailVerified, lastLoginAt
58
- - Relationships: ManyToMany(Role), OneToMany(Session), OneToMany(PasswordReset), OneToMany(TwoFactorAuth)
59
-
60
- - **Role** - User roles (STUDENT, INSTRUCTOR, TA, ADMIN, IT_ADMIN)
61
- - Many-to-Many relationship with User
62
- - Has many Permissions
63
-
64
- - **Permission** - Fine-grained access control
65
- - Linked to Roles
66
-
67
- - **Session** - Active user sessions with tokens
68
- - userId, refreshToken, userAgent, ipAddress, expiresAt
69
-
70
- - **PasswordReset** - Password reset tokens
71
- - userId, token, expiresAt
72
-
73
- - **TwoFactorAuth** - 2FA configuration
74
- - userId, secret, isEnabled, backupCodes
75
-
76
- - **EmailVerification** - Email verification tokens
77
- - userId, token, expiresAt
78
-
79
- - **UserPreference** - User UI/notification preferences
80
- - userId, theme, notifications, language
81
-
82
- #### DTOs (5):
83
- - **user.dto** - User profile data transfer
84
- - **user-response.dto** - Response format for user endpoints
85
- - **user-management.dto** - Admin operations
86
- - **register-request.dto** - Registration validation
87
- - **login-request.dto** - Login credentials
88
- - **auth-response.dto** - Auth token response
89
-
90
- #### Controllers (3):
91
- - **auth.controller** (@Controller('api/auth'))
92
- - POST /register - Public user registration
93
- - POST /login - Public login
94
- - POST /logout - Logout (requires JWT)
95
- - POST /refresh-token - Refresh access token
96
- - POST /forgot-password - Request password reset
97
- - POST /reset-password - Reset with token
98
- - GET /me - Get current user profile
99
-
100
- - **user-management.controller** (@Controller('api/admin'))
101
- - User CRUD operations for admins
102
-
103
- - **user-profile.controller** (@Controller('api/users'))
104
- - Profile management endpoints
105
-
106
- #### Services (2):
107
- - **auth.service** - Authentication logic, JWT handling
108
- - **user-management.service** - User CRUD & role assignment
109
-
110
- ---
111
-
112
- ### 2️⃣ COURSES MODULE
113
- **Location:** \src/modules/courses/\
114
-
115
- #### Entities (4):
116
- - **Course**
117
- - Fields: courseId, departmentId, name, code, description, credits, level (enum), syllabusUrl, status (ACTIVE/ARCHIVED)
118
- - Relationships: ManyToOne(Department), OneToMany(CoursePrerequisite), OneToMany(CourseSection)
119
-
120
- - **CourseSection**
121
- - Fields: sectionId, courseId, semesterId, sectionNumber, maxCapacity, currentEnrollment, location, status (OPEN/CLOSED/FULL)
122
- - Relationships: ManyToOne(Course), ManyToOne(Semester), OneToMany(CourseSchedule)
123
-
124
- - **CourseSchedule**
125
- - Class meeting times and locations for sections
126
-
127
- - **CoursePrerequisite**
128
- - Links prerequisite courses to courses
129
-
130
- #### DTOs (4):
131
- - **course.dto** - Create/Update course
132
- - **section.dto** - Section management
133
- - **schedule.dto** - Schedule management
134
- - **prerequisite.dto** - Prerequisite relationships
135
-
136
- #### Controllers (3):
137
- - **courses.controller** (@Controller('api/courses'))
138
- - GET / - List all courses with filters
139
- - GET /department/:deptId - List courses by department
140
- - GET /:id - Get course details
141
- - POST / - Create course (INSTRUCTOR/ADMIN)
142
- - PATCH /:id - Update course
143
- - DELETE /:id - Delete course
144
-
145
- - **course-sections.controller** (@Controller('api/sections'))
146
- - GET / - List sections
147
- - POST / - Create section
148
- - GET /:id - Get section details
149
- - PATCH /:id - Update section
150
- - DELETE /:id - Delete section
151
-
152
- - **course-schedules.controller** (@Controller('api/schedules'))
153
- - Schedule management for course sections
154
-
155
- #### Services (3):
156
- - **courses.service** - Course CRUD & filtering
157
- - **course-sections.service** - Section management
158
- - **course-schedules.service** - Schedule management
159
-
160
- ---
161
-
162
- ## Academic Modules
163
-
164
- ### 3️⃣ ENROLLMENTS MODULE
165
- **Location:** \src/modules/enrollments/\
166
-
167
- #### Entities (3):
168
- - **CourseEnrollment**
169
- - Fields: enrollmentId, userId, sectionId, programId, status (ENROLLED/DROPPED/COMPLETED), grade, finalScore
170
- - Relationships: ManyToOne(User), ManyToOne(CourseSection), ManyToOne(Program)
171
-
172
- - **CourseInstructor**
173
- - Link instructors to course sections
174
-
175
- - **CourseTA**
176
- - Link teaching assistants to course sections
177
-
178
- #### DTOs (3):
179
- - **enrollment-response.dto**
180
- - **enroll-course.dto**
181
- - **available-courses.dto**
182
- - **drop-course.dto**
183
-
184
- #### Controllers (1):
185
- - **enrollments.controller** (@Controller('api/enrollments'))
186
- - GET /my-courses - Get student's enrolled courses
187
- - GET /available - Get available courses to enroll
188
- - POST / - Enroll in course
189
- - DELETE /:enrollmentId - Drop course
190
-
191
- #### Services (1):
192
- - **enrollments.service** - Enrollment CRUD & validation
193
-
194
- ---
195
-
196
- ### 4️⃣ ASSIGNMENTS MODULE
197
- **Location:** \src/modules/assignments/\
198
-
199
- #### Entities (2):
200
- - **Assignment**
201
- - Fields: assignmentId, courseId, title, description, instructions, maxScore, weight, dueDate, availableFrom, lateSubmissionAllowed, latePenaltyPercent, submissionType (FILE/TEXT/CODE), maxFileSizeMb, allowedFileTypes, status (DRAFT/PUBLISHED/CLOSED), createdBy
202
- - Relationships: ManyToOne(Course), ManyToOne(User), OneToMany(AssignmentSubmission)
203
-
204
- - **AssignmentSubmission**
205
- - Student submission with files, grade, feedback
206
-
207
- #### DTOs (5):
208
- - **create-assignment.dto**
209
- - **update-assignment.dto**
210
- - **submit-assignment.dto**
211
- - **grade-submission.dto**
212
- - **assignment-query.dto**
213
-
214
- #### Controllers (1):
215
- - **assignments.controller** (@Controller('api/assignments'))
216
- - GET / - List assignments with filters
217
- - POST / - Create assignment (INSTRUCTOR/ADMIN)
218
- - GET /:id - Get assignment details
219
- - PATCH /:id - Update assignment
220
- - DELETE /:id - Delete assignment
221
- - POST /:id/submit - Submit assignment
222
- - POST /:submissionId/grade - Grade submission
223
-
224
- #### Services (1):
225
- - **assignments.service** - CRUD & grading
226
-
227
- ---
228
-
229
- ### 5️⃣ QUIZZES MODULE
230
- **Location:** \src/modules/quizzes/\
231
-
232
- #### Entities (5):
233
- - **Quiz**
234
- - Fields: quizId, courseId, title, description, instructions, quizType (GRADED/PRACTICE), timeLimitMinutes, maxAttempts, passingScore, randomizeQuestions, showCorrectAnswers, showAnswersAfter (enum), availableFrom, availableUntil, weight, createdBy
235
- - Relationships: ManyToOne(Course), ManyToOne(User), OneToMany(QuizQuestion), OneToMany(QuizAttempt)
236
-
237
- - **QuizQuestion**
238
- - Different question types with options and correct answers
239
-
240
- - **QuizAttempt**
241
- - Student quiz attempt with start/end times, score
242
-
243
- - **QuizAnswer**
244
- - Individual answers to questions
245
-
246
- - **QuizDifficultyLevel**
247
- - Question difficulty tracking
248
-
249
- #### DTOs (10):
250
- - **create-quiz.dto**, **update-quiz.dto**
251
- - **create-question.dto**, **update-question.dto**
252
- - **start-attempt.dto**, **submit-quiz.dto**
253
- - **quiz-query.dto**, **quiz-results.dto**
254
- - **manual-grade.dto**, **reorder-questions.dto**
255
-
256
- #### Controllers (1):
257
- - **quizzes.controller** (@Controller('quizzes'))
258
- - POST / - Create quiz (INSTRUCTOR/TA/ADMIN)
259
- - GET / - List quizzes
260
- - GET /:quizId - Get quiz details
261
- - PATCH /:quizId - Update quiz
262
- - DELETE /:quizId - Delete quiz
263
- - POST /:quizId/start - Start attempt
264
- - POST /:quizId/submit - Submit answers
265
- - GET /:quizId/results/:attemptId - Get attempt results
266
-
267
- #### Services (2):
268
- - **quizzes.service** - Quiz CRUD & attempt management
269
- - **quiz-grading.service** - Auto & manual grading
270
-
271
- ---
272
-
273
- ### 6️⃣ GRADES MODULE
274
- **Location:** \src/modules/grades/\
275
-
276
- #### Entities (5):
277
- - **Grade**
278
- - Fields: gradeId, userId, courseId, gradeType (ASSIGNMENT/QUIZ/LAB/EXAM/PARTICIPATION), assignmentId, quizId, labId, score, maxScore, percentage, letterGrade, feedback, gradedBy, gradedAt, isPublished
279
- - Relationships: ManyToOne(User), ManyToOne(Course), ManyToOne(Assignment), ManyToOne(Quiz), ManyToOne(Lab), ManyToOne(User-Grader)
280
-
281
- - **GradeComponent**
282
- - Breakdown of grade components
283
-
284
- - **Rubric**
285
- - Grading rubric for assignments
286
-
287
- - **RubricCriteria**
288
- - Individual criteria within rubric
289
-
290
- - **GpaCalculation**
291
- - GPA calculations per student/semester
292
-
293
- #### DTOs (4):
294
- - **create-grade.dto**, **update-grade.dto**
295
- - **create-rubric.dto**
296
- - **grade-query.dto**
297
- - **transcript-response.dto**
298
-
299
- #### Controllers (2):
300
- - **grades.controller** (@Controller('api/grades'))
301
- - GET / - List grades (INSTRUCTOR/TA/ADMIN)
302
- - GET /my - Get student's grades (STUDENT)
303
- - GET /transcript/:studentId - Get transcript
304
- - GET /gpa/:studentId - Calculate GPA
305
- - GET /distribution/:courseId - Grade distribution
306
- - PUT /:id - Update grade
307
-
308
- - **rubrics.controller** (@Controller('api/rubrics'))
309
- - Rubric CRUD operations
310
-
311
- #### Services (2):
312
- - **grades.service** - Grade CRUD, GPA calculation, transcript
313
- - **rubrics.service** - Rubric management
314
-
315
- ---
316
-
317
- ### 7️⃣ ATTENDANCE MODULE
318
- **Location:** \src/modules/attendance/\
319
-
320
- #### Entities (4):
321
- - **AttendanceSession**
322
- - Class session for attendance tracking
323
-
324
- - **AttendanceRecord**
325
- - Fields: recordId, sessionId, userId, attendanceStatus (PRESENT/ABSENT/LATE/EXCUSED), checkInTime, markedBy (MANUAL/AI/SYSTEM), confidenceScore, notes
326
- - Relationships: ManyToOne(AttendanceSession), ManyToOne(User)
327
-
328
- - **AttendancePhoto**
329
- - Photos for AI-based attendance
330
-
331
- - **AiAttendanceProcessing**
332
- - AI processing status and results
333
-
334
- #### DTOs (8):
335
- - **create-session.dto**, **update-session.dto**
336
- - **mark-attendance.dto**, **batch-attendance.dto**
337
- - **update-record.dto**
338
- - **attendance-query.dto**, **attendance-summary.dto**
339
- - **import-attendance.dto**
340
-
341
- #### Controllers (1):
342
- - **attendance.controller** (@Controller('attendance'))
343
- - POST /sessions - Create session
344
- - PATCH /sessions/:id - Update session
345
- - POST /mark - Mark attendance
346
- - POST /batch - Batch mark attendance
347
- - GET /sessions/:sessionId - Get session records
348
- - POST /import - Import from Excel
349
- - GET /summary/:courseId - Attendance summary
350
-
351
- #### Services (3):
352
- - **attendance.service** - Attendance CRUD
353
- - **attendance-excel.service** - Excel import/export
354
- - **attendance-ai.service** - AI photo recognition
355
-
356
- ---
357
-
358
- ### 8️⃣ LABS MODULE
359
- **Location:** \src/modules/labs/\
360
-
361
- #### Entities (4):
362
- - **Lab**
363
- - Lab assignments/projects
364
-
365
- - **LabSubmission**
366
- - Student lab submissions with grading
367
-
368
- - **LabInstruction**
369
- - Lab instructions and resources
370
-
371
- - **LabAttendance**
372
- - Lab attendance tracking
373
-
374
- #### DTOs (Multiple)
375
- - Various lab management DTOs
376
-
377
- #### Controllers (1):
378
- - **labs.controller** (@Controller('api/labs'))
379
- - Lab CRUD and submission operations
380
-
381
- #### Services (1):
382
- - **labs.service** - Lab management
383
-
384
- ---
385
-
386
- ## Community & Discussion Modules
387
-
388
- ### 9️⃣ COMMUNITY MODULE
389
- **Location:** \src/modules/community/\
390
-
391
- #### Entities (6):
392
- - **Community**
393
- - General community groups/forums
394
-
395
- - **CommunityPost**
396
- - Fields: postId, courseId, communityId, userId, title, content, postType (DISCUSSION/QUESTION/ANNOUNCEMENT/POLL), isPinned, isLocked, viewCount, upvoteCount, replyCount
397
- - Relationships: ManyToOne(User), ManyToOne(Course), ManyToOne(Community), OneToMany(CommunityComment), OneToMany(CommunityReaction), ManyToMany(CommunityTag)
398
-
399
- - **CommunityComment**
400
- - Comments on posts with nested replies
401
-
402
- - **CommunityReaction**
403
- - Like/upvote reactions to posts
404
-
405
- - **CommunityTag**
406
- - Tags for categorizing posts
407
-
408
- - **ForumCategory**
409
- - Forum categories/sections
410
-
411
- #### DTOs (12):
412
- - **create-post.dto**, **update-post.dto**, **post-query.dto**
413
- - **create-comment.dto**, **update-comment.dto**
414
- - **create-reaction.dto**
415
- - **create-tag.dto**
416
- - **create-community.dto**, **update-community.dto**
417
- - **create-category.dto**, **update-category.dto**
418
- - **community-query.dto**
419
-
420
- #### Controllers (4):
421
- - **communities.controller** (@Controller('api/community'))
422
- - CRUD for communities
423
-
424
- - **community-posts.controller** (@Controller('api/community/posts'))
425
- - GET / - List posts with filters
426
- - POST / - Create post
427
- - GET /:postId - Get post
428
- - PATCH /:postId - Update post
429
- - DELETE /:postId - Delete post
430
-
431
- - **community-comments.controller** (@Controller('api/community/comments'))
432
- - Comments CRUD
433
-
434
- - **forum-categories.controller** (@Controller('api/community/categories'))
435
- - Category management
436
-
437
- #### Services (5):
438
- - **communities.service**
439
- - **community-posts.service**
440
- - **community-comments.service**
441
- - **community-tags.service**
442
- - **forum-categories.service**
443
-
444
- ---
445
-
446
- ### 🔟 DISCUSSIONS MODULE
447
- **Location:** \src/modules/discussions/\
448
-
449
- #### Entities (2):
450
- - **CourseChatThread**
451
- - Course-specific discussion threads
452
-
453
- - **ChatMessage**
454
- - Individual chat messages
455
-
456
- #### DTOs (Multiple)
457
- - Discussion management DTOs
458
-
459
- #### Controllers (1):
460
- - **discussions.controller** (@Controller('api/discussions'))
461
- - Thread and message CRUD
462
-
463
- #### Services (1):
464
- - **discussions.service**
465
-
466
- ---
467
-
468
- ### 1️⃣1️⃣ ANNOUNCEMENTS MODULE
469
- **Location:** \src/modules/announcements/\
470
-
471
- #### Entities (1):
472
- - **Announcement**
473
- - Course/system announcements with scheduling
474
-
475
- #### DTOs (4):
476
- - **create-announcement.dto**
477
- - **update-announcement.dto**
478
- - **schedule-announcement.dto**
479
- - **announcement-query.dto**
480
-
481
- #### Controllers (1):
482
- - **announcements.controller** (@Controller('api/announcements'))
483
- - GET / - List announcements
484
- - POST / - Create (INSTRUCTOR/ADMIN)
485
- - GET /:id - Get details
486
- - PATCH /:id - Update
487
- - DELETE /:id - Delete
488
- - POST /:id/schedule - Schedule announcement
489
-
490
- #### Services (1):
491
- - **announcements.service**
492
-
493
- ---
494
-
495
- ## Administrative Modules
496
-
497
- ### 1️⃣2️⃣ CAMPUS MODULE
498
- **Location:** \src/modules/campus/\
499
-
500
- #### Entities (4):
501
- - **Campus**
502
- - Fields: campusId, name, code (unique), address, city, country, phone, email, timezone, status (ACTIVE/INACTIVE)
503
- - Relationships: OneToMany(Department)
504
-
505
- - **Department**
506
- - Fields: departmentId, campusId, name, code, phone, email, status
507
- - Relationships: ManyToOne(Campus)
508
-
509
- - **Program**
510
- - Degree/program offerings
511
-
512
- - **Semester**
513
- - Academic semester/term information
514
-
515
- #### DTOs (4):
516
- - **campus.dto**, **department.dto**, **program.dto**, **semester.dto**
517
-
518
- #### Controllers (4):
519
- - **campus.controller** (@Controller('api/campuses'))
520
- - Campus CRUD
521
-
522
- - **department.controller** (@Controller('api'))
523
- - Department CRUD
524
-
525
- - **program.controller** (@Controller('api'))
526
- - Program CRUD
527
-
528
- - **semester.controller** (@Controller('api/semesters'))
529
- - Semester CRUD
530
-
531
- #### Services (4):
532
- - **campus.service**
533
- - **department.service**
534
- - **program.service**
535
- - **semester.service**
536
-
537
- ---
538
-
539
- ### 1️⃣3️⃣ ATTENDANCE MODULE (Already covered - see Academic Modules)
540
-
541
- ### 1️⃣4️⃣ REPORTS MODULE
542
- **Location:** \src/modules/reports/\
543
-
544
- #### Entities (3):
545
- - **GeneratedReport**
546
- - Generated reports for analysis
547
-
548
- - **ReportTemplate**
549
- - Report templates for customization
550
-
551
- - **ExportHistory**
552
- - Track exported reports
553
-
554
- #### DTOs (2):
555
- - **generate-report.dto**
556
- - **report-query.dto**
557
-
558
- #### Controllers (1):
559
- - **reports.controller** (@Controller('api/reports'))
560
- - Report generation and export
561
-
562
- #### Services (1):
563
- - **reports.service**
564
-
565
- ---
566
-
567
- ## Infrastructure Modules
568
-
569
- ### 1️⃣5️⃣ FILES MODULE
570
- **Location:** \src/modules/files/\
571
-
572
- #### Entities (4):
573
- - **Folder**
574
- - User/course file folders
575
-
576
- - **File**
577
- - Files with metadata and versioning
578
-
579
- - **FileVersion**
580
- - File version history
581
-
582
- - **FilePermission**
583
- - File-level permissions
584
-
585
- #### DTOs (9):
586
- - **upload-file.dto**, **create-folder.dto**
587
- - **file-response.dto**, **folder-response.dto**
588
- - **file-version-response.dto**
589
- - **file-permission.dto**, **grant-permission.dto**
590
- - **file-search.dto**, **update-folder.dto**
591
-
592
- #### Controllers (2):
593
- - **files.controller** (@Controller('api/files'))
594
- - GET / - List files
595
- - POST / - Upload file
596
- - GET /:fileId - Download file
597
- - DELETE /:fileId - Delete file
598
-
599
- - **folder.controller** (@Controller('api/files/folders'))
600
- - Folder CRUD operations
601
-
602
- #### Services (4):
603
- - **files.service** - File CRUD
604
- - **folder.service** - Folder management
605
- - **file-storage.service** - S3/Cloud storage
606
- - **file-permission.service** - Permission management
607
-
608
- ---
609
-
610
- ### 1️⃣6️⃣ NOTIFICATIONS MODULE
611
- **Location:** \src/modules/notifications/\
612
-
613
- #### Entities (3):
614
- - **Notification**
615
- - Fields: notificationId, userId, notificationType (announcement/grade/assignment/message/deadline/system), title, body, relatedEntityType, relatedEntityId, isRead, readAt, priority (low/medium/high/urgent), actionUrl
616
- - Relationships: ManyToOne(User)
617
-
618
- - **NotificationPreference**
619
- - User notification settings
620
-
621
- - **ScheduledNotification**
622
- - Scheduled notifications
623
-
624
- #### DTOs (Multiple)
625
- - Notification management DTOs
626
-
627
- #### Controllers (1):
628
- - **notifications.controller** (@Controller('api/notifications'))
629
- - Notification CRUD and preferences
630
-
631
- #### Services (1):
632
- - **notifications.service**
633
-
634
- ---
635
-
636
- ### 1️⃣7️⃣ MESSAGING MODULE
637
- **Location:** \src/modules/messaging/\
638
-
639
- #### Entities (2):
640
- - **Message**
641
- - Fields: messageId, senderId, subject, body, messageType (direct/group/announcement), parentMessageId, replyToId, readStatus, sentAt, editedAt
642
- - Relationships: ManyToOne(Message), OneToMany(Message), OneToMany(MessageParticipant)
643
-
644
- - **MessageParticipant**
645
- - Link messages to recipient users
646
-
647
- #### DTOs (Multiple)
648
- - Message management DTOs
649
-
650
- #### Controllers (1):
651
- - **messaging.controller** (@Controller('api/messages'))
652
- - Message CRUD with threading
653
-
654
- #### Services (1):
655
- - **messaging.service** - Message operations
656
-
657
- ---
658
-
659
- ### 1️⃣8️⃣ SCHEDULE MODULE
660
- **Location:** \src/modules/schedule/\
661
-
662
- #### Entities (3):
663
- - **ExamSchedule**
664
- - Exam scheduling for courses
665
-
666
- - **CalendarEvent**
667
- - Generic calendar events
668
-
669
- - **CalendarIntegration**
670
- - Integration with Google Calendar/Outlook
671
-
672
- #### DTOs (8):
673
- - **create-exam-schedule.dto**, **update-exam-schedule.dto**
674
- - **create-calendar-event.dto**, **update-calendar-event.dto**
675
- - **query-exam-schedule.dto**, **query-calendar-event.dto**
676
- - **connect-calendar.dto**, **query-schedule.dto**
677
-
678
- #### Controllers (4):
679
- - **schedule.controller** (@Controller('api/schedule'))
680
- - **exam-schedule.controller** (@Controller('api/exams/schedule'))
681
- - **calendar-events.controller** (@Controller('api/calendar/events'))
682
- - **calendar-integrations.controller** (@Controller('api/calendar/integrations'))
683
-
684
- #### Services (4):
685
- - **schedule.service**
686
- - **exam-schedule.service**
687
- - **calendar-events.service**
688
- - **calendar-integrations.service**
689
-
690
- ---
691
-
692
- ### 1️⃣9️⃣ COURSE MATERIALS MODULE
693
- **Location:** \src/modules/course-materials/\
694
-
695
- #### Entities (2):
696
- - **CourseMaterial**
697
- - Lecture notes, slides, videos, documents
698
-
699
- - **LectureSectionLab**
700
- - Structure linking lectures/sections/labs
701
-
702
- #### DTOs (6):
703
- - **create-material.dto**, **update-material.dto**
704
- - **bulk-create-material.dto**
705
- - **query-materials.dto**
706
- - **create-structure.dto**, **update-structure.dto**
707
- - **reorder-structure.dto**, **toggle-visibility.dto**
708
- - **upload-video-material.dto**
709
-
710
- #### Controllers (2):
711
- - **materials.controller** (@Controller('api/courses/:courseId/materials'))
712
- - GET / - List materials
713
- - POST / - Upload material
714
- - PATCH /:materialId - Update material
715
- - DELETE /:materialId - Delete material
716
-
717
- - **course-structure.controller** (@Controller('api/courses/:courseId/structure'))
718
- - Course structure/organization
719
-
720
- #### Services (2):
721
- - **materials.service**
722
- - **course-structure.service**
723
-
724
- ---
725
-
726
- ### 2️⃣0️⃣ TASKS MODULE
727
- **Location:** \src/modules/tasks/\
728
-
729
- #### Entities (3):
730
- - **StudentTask**
731
- - Student task management
732
-
733
- - **TaskCompletion**
734
- - Task completion tracking
735
-
736
- - **DeadlineReminder**
737
- - Deadline reminder notifications
738
-
739
- #### DTOs (4):
740
- - **create-task.dto**, **update-task.dto**
741
- - **task-query.dto**
742
- - **create-reminder.dto**
743
-
744
- #### Controllers (1):
745
- - **tasks.controller** (@Controller('api/tasks'))
746
- - Task CRUD and deadline reminders
747
-
748
- #### Services (2):
749
- - **tasks.service**
750
- - **reminders.service** - Deadline reminders
751
-
752
- ---
753
-
754
- ### 2️⃣1️⃣ ANALYTICS MODULE
755
- **Location:** \src/modules/analytics/\
756
-
757
- #### Entities (6):
758
- - **CourseAnalytics**
759
- - Course engagement metrics
760
-
761
- - **LearningAnalytics**
762
- - Learning behavior tracking
763
-
764
- - **PerformanceMetrics**
765
- - Student performance data
766
-
767
- - **StudentProgress**
768
- - Course progress tracking
769
-
770
- - **WeakTopicsAnalysis**
771
- - Weak topic identification for students
772
-
773
- - **ActivityLog**
774
- - System activity logging
775
-
776
- #### DTOs (2):
777
- - **analytics-query.dto**
778
- - **student-analytics-query.dto**
779
-
780
- #### Controllers (1):
781
- - **analytics.controller** (@Controller('api/analytics'))
782
- - GET /courses/:courseId - Course analytics
783
- - GET /students/:studentId - Student analytics
784
- - GET /class/:courseId - Class analytics
785
-
786
- #### Services (2):
787
- - **analytics.service** - Analytics calculations
788
- - **analytics-cron.service** - Scheduled analytics processing
789
-
790
- ---
791
-
792
- ### 2️⃣2️⃣ SEARCH MODULE
793
- **Location:** \src/modules/search/\
794
-
795
- #### Entities (2):
796
- - **SearchIndex**
797
- - Search index for full-text search
798
-
799
- - **SearchHistory**
800
- - User search history
801
-
802
- #### DTOs (2):
803
- - **global-search.dto**
804
- - **search-history-query.dto**
805
-
806
- #### Controllers (1):
807
- - **search.controller** (@Controller('api/search'))
808
- - Global search across courses, materials, announcements
809
-
810
- #### Services (1):
811
- - **search.service**
812
-
813
- ---
814
-
815
- ### 2️⃣3️⃣ EMAIL MODULE
816
- **Location:** \src/modules/email/\
817
-
818
- #### Entities: None (Service-only)
819
-
820
- #### DTOs: None
821
-
822
- #### Controllers: None
823
-
824
- #### Services (1):
825
- - **email.service** - Email sending for notifications, passwords, etc.
826
-
827
- ---
828
-
829
- ### 2️⃣4️⃣ YOUTUBE MODULE
830
- **Location:** \src/modules/youtube/\
831
-
832
- #### Entities: None
833
-
834
- #### DTOs: None
835
-
836
- #### Controllers (1):
837
- - **youtube.controller** (@Controller('youtube'))
838
- - YouTube integration for video uploads
839
-
840
- #### Services (1):
841
- - **youtube.service** - YouTube API integration
842
-
843
- ---
844
-
845
- ## Key Relationships Summary
846
-
847
- ### User-Centric Relationships:
848
- - User → Roles (Many-to-Many)
849
- - User → CourseEnrollment (One-to-Many)
850
- - User → Grade (One-to-Many)
851
- - User → Assignment/Quiz/Lab (Many creators)
852
- - User → Message (One-to-Many as sender)
853
- - User → Notification (One-to-Many)
854
-
855
- ### Course-Centric Relationships:
856
- - Course → Department (Many-to-One)
857
- - Course → CourseSection (One-to-Many)
858
- - Course → CourseEnrollment (One-to-Many via Section)
859
- - Course → Assignment/Quiz/Lab (One-to-Many)
860
- - Course → Grade (One-to-Many)
861
- - Course → CourseMaterial (One-to-Many)
862
- - Course → Announcement (One-to-Many)
863
-
864
- ### Academic Assessment Flow:
865
- - Assignment → AssignmentSubmission → Grade
866
- - Quiz → QuizAttempt → QuizAnswer → Grade
867
- - Lab → LabSubmission → Grade
868
- - All grades rollup to CourseEnrollment.finalScore
869
-
870
- ### Enrollment Flow:
871
- - User → CourseEnrollment (Many-to-Many via CourseSection)
872
- - CourseEnrollment → Grade (One-to-Many)
873
- - CourseEnrollment.status → ENROLLED/DROPPED/COMPLETED
874
-
875
- ---
876
-
877
- ## Key Features by Module
878
-
879
- ### Authentication & Security:
880
- - JWT token-based authentication
881
- - Role-based access control (RBAC)
882
- - Password hashing with bcrypt
883
- - Email verification
884
- - Password reset tokens
885
- - 2FA support
886
- - Session management
887
-
888
- ### Academic Management:
889
- - Course enrollment with prerequisites
890
- - Assignment submissions with late penalties
891
- - Quiz creation with adaptive difficulty
892
- - Grade tracking and GPA calculation
893
- - Lab sessions and submissions
894
- - Attendance tracking with AI recognition
895
-
896
- ### Communication:
897
- - Direct messaging with threading
898
- - Course discussions
899
- - Announcements with scheduling
900
- - Notifications with preferences
901
- - Email integration
902
-
903
- ### Community & Engagement:
904
- - Community forums with categories
905
- - Posts with reactions and comments
906
- - Tagging system
907
- - Pinned/locked posts
908
- - Reputation system (upvotes)
909
-
910
- ### Administrative:
911
- - Multi-campus support
912
- - Department and program management
913
- - Academic calendar (semesters)
914
- - User management (creation, role assignment)
915
- - Report generation and export
916
- - Analytics and performance tracking
917
-
918
- ### Infrastructure:
919
- - File management with versioning
920
- - File-level permissions
921
- - Integration with cloud storage
922
- - Calendar integration (Google/Outlook)
923
- - Search functionality
924
- - Activity logging
925
- - Scheduled notifications/reminders
926
-
927
- ---
928
-
929
- ## Architecture Patterns
930
-
931
- ### Module Structure Pattern:
932
- Each module follows: Entity → DTO → Controller → Service → Repository (TypeORM)
933
-
934
- ### Database Constraints:
935
- - Unique indexes for key fields
936
- - Foreign key relationships with CASCADE/SET NULL
937
- - Soft deletes with DeleteDateColumn
938
- - Audit trails with CreateDateColumn/UpdateDateColumn
939
-
940
- ### Authentication Guards:
941
- - JwtAuthGuard - Validates JWT tokens
942
- - RolesGuard - Enforces role-based access
943
- - @Public() decorator for public endpoints
944
- - @Roles() decorator for role requirements
945
-
946
- ### Status Enums:
947
- - User: ACTIVE, INACTIVE, SUSPENDED, PENDING
948
- - Course: ACTIVE, ARCHIVED
949
- - Section: OPEN, CLOSED, FULL
950
- - Assignment: DRAFT, PUBLISHED, CLOSED
951
- - Enrollment: ENROLLED, DROPPED, COMPLETED
952
- - Grade: Published/Unpublished
953
- - Notification: Read/Unread
954
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
DATABASE_SCHEMA.md DELETED
@@ -1,508 +0,0 @@
1
- # 🗄️ EduVerse Backend - Database Schema & Entity Relationships
2
-
3
- ## Core Entity Relationships
4
-
5
- ### USER HIERARCHY
6
- \\\
7
- User (userId)
8
- ├── Roles (Many-to-Many)
9
- ├── Sessions (One-to-Many)
10
- ├── PasswordReset (One-to-Many)
11
- ├── TwoFactorAuth (One-to-Many)
12
- ├── UserPreference (One-to-One)
13
- ├── EmailVerification (One-to-One)
14
- ├── CourseEnrollment (One-to-Many)
15
- ├── Grade (One-to-Many)
16
- ├── Assignment (One-to-Many, as creator)
17
- ├── Quiz (One-to-Many, as creator)
18
- ├── Lab (One-to-Many, as creator)
19
- ├── CommunityPost (One-to-Many, as author)
20
- ├── Message (One-to-Many, as sender)
21
- ├── Notification (One-to-Many)
22
- └── AttendanceRecord (One-to-Many)
23
- \\\
24
-
25
- ### COURSE STRUCTURE
26
- \\\
27
- Campus (campusId)
28
- └── Department (One-to-Many)
29
- └── Course (One-to-Many)
30
- ├── CoursePrerequisite (One-to-Many, prerequisites)
31
- ├── CourseSection (One-to-Many)
32
- │ ├── CourseSchedule (One-to-Many)
33
- │ ├── CourseEnrollment (One-to-Many)
34
- │ │ ├── CourseInstructor
35
- │ │ ├── CourseTA
36
- │ │ └── User (Student)
37
- │ └── AttendanceSession (One-to-Many)
38
- │ └── AttendanceRecord (One-to-Many)
39
- ├── Announcement (One-to-Many)
40
- ├── CourseMaterial (One-to-Many)
41
- ├── CourseChatThread (One-to-Many)
42
- ├── CommunityPost (One-to-Many)
43
- ├── Quiz (One-to-Many)
44
- ├── Assignment (One-to-Many)
45
- ├── Lab (One-to-Many)
46
- └── Grade (One-to-Many)
47
-
48
- Semester (semesterId)
49
- └── CourseSection (One-to-Many)
50
- \\\
51
-
52
- ### ACADEMIC ASSESSMENT FLOW
53
- \\\
54
- User (Student) → CourseEnrollment → Course
55
-
56
- ┌─────────────┼─────────────┐
57
- ↓ ↓ ↓
58
- Assignment Quiz Lab
59
- ↓ ↓ ↓
60
- AssignmentSubmission QuizAttempt LabSubmission
61
- ↓ ↓ ↓
62
- Grade ←────────┴─────────────┘
63
-
64
- GradeComponent
65
- RubricCriteria
66
- GPACalculation
67
- \\\
68
-
69
- ### ENROLLMENT & GRADING
70
- \\\
71
- CourseEnrollment (enrollmentId)
72
- ├── User (userId)
73
- ├── CourseSection (sectionId)
74
- ├── Program (programId)
75
- └── Grade (One-to-Many for same user/course)
76
- ├── Assignment (links to assignment)
77
- ├── Quiz (links to quiz)
78
- └── Lab (links to lab)
79
- \\\
80
-
81
- ### QUIZ STRUCTURE
82
- \\\
83
- Quiz (quizId)
84
- ├── Course (courseId)
85
- ├── CreatedBy (userId)
86
- ├── QuizQuestion (One-to-Many)
87
- │ ├── QuizDifficultyLevel
88
- │ └── QuizAnswer (One-to-Many, correct answers)
89
- └── QuizAttempt (One-to-Many, by students)
90
- ├── User (userId)
91
- ├── QuizAnswer (One-to-Many, student answers)
92
- └── Grade (linked)
93
- \\\
94
-
95
- ### MESSAGING & COMMUNICATION
96
- \\\
97
- Message (messageId)
98
- ├── Sender (userId)
99
- ├── ParentMessage (self-join for threads)
100
- ├── Replies (One-to-Many, self-join)
101
- └── MessageParticipant (One-to-Many)
102
- └── User (recipients)
103
-
104
- CourseChatThread (threadId)
105
- ├── Course (courseId)
106
- └── ChatMessage (One-to-Many)
107
- ├── Sender (userId)
108
- └── CreatedAt (timestamp)
109
- \\\
110
-
111
- ### COMMUNITY & FORUMS
112
- \\\
113
- Community (communityId)
114
- └── CommunityPost (One-to-Many)
115
- ├── Author (userId)
116
- ├── Course (optional)
117
- ├── CommunityComment (One-to-Many)
118
- │ ├── Author (userId)
119
- │ └── Replies (self-join)
120
- ├── CommunityReaction (One-to-Many)
121
- │ └── User (userId)
122
- └── CommunityTag (Many-to-Many)
123
-
124
- ForumCategory (categoryId)
125
- └── CommunityPost (One-to-Many)
126
- \\\
127
-
128
- ### FILES & STORAGE
129
- \\\
130
- Folder (folderId)
131
- └── File (One-to-Many)
132
- ├── FileVersion (One-to-Many)
133
- └── FilePermission (One-to-Many)
134
- └── User (userId)
135
- \\\
136
-
137
- ### NOTIFICATIONS & SCHEDULING
138
- \\\
139
- Notification (notificationId)
140
- ├── User (userId)
141
- ├── NotificationPreference (One-to-One)
142
- └── ScheduledNotification (One-to-Many)
143
-
144
- Announcement (announcementId)
145
- └── Notification (One-to-Many, linked)
146
-
147
- DeadlineReminder (reminderId)
148
- ├── StudentTask (taskId)
149
- └── User (userId)
150
- \\\
151
-
152
- ### ANALYTICS & REPORTING
153
- \\\
154
- CourseAnalytics
155
- ├── Course (courseId)
156
- └── PerformanceMetrics (One-to-Many)
157
-
158
- StudentProgress
159
- ├── User (userId)
160
- ├── Course (courseId)
161
- └── WeakTopicsAnalysis (One-to-Many)
162
-
163
- LearningAnalytics
164
- ├── User (userId)
165
- └── ActivityLog (One-to-Many)
166
-
167
- GeneratedReport
168
- ├── User (userId)
169
- └── ReportTemplate (templateId)
170
- \\\
171
-
172
- ### CALENDAR & SCHEDULING
173
- \\\
174
- CourseSchedule (course_schedules)
175
- ├── CourseSection (sectionId)
176
- └���─ Day/Time info
177
-
178
- ExamSchedule (exam_schedules)
179
- ├── Course (courseId)
180
- └── Exam date/time
181
-
182
- CalendarEvent (calendar_events)
183
- ├── User (userId)
184
- └── CalendarIntegration (integrationId)
185
- ├── Google Calendar
186
- └── Outlook Calendar
187
- \\\
188
-
189
- ---
190
-
191
- ## Database Tables (70+ total)
192
-
193
- ### AUTH TABLES (8)
194
- - users
195
- - roles
196
- - permissions
197
- - user_roles (junction)
198
- - sessions
199
- - password_resets
200
- - two_factor_auths
201
- - email_verifications
202
- - user_preferences
203
-
204
- ### CAMPUS & ORGANIZATIONAL (4)
205
- - campuses
206
- - departments
207
- - programs
208
- - semesters
209
-
210
- ### COURSES & ENROLLMENT (7)
211
- - courses
212
- - course_sections
213
- - course_schedules
214
- - course_prerequisites
215
- - course_enrollments
216
- - course_instructors
217
- - course_tas
218
-
219
- ### ACADEMIC ASSESSMENT (12)
220
- - assignments
221
- - assignment_submissions
222
- - quizzes
223
- - quiz_questions
224
- - quiz_attempts
225
- - quiz_answers
226
- - quiz_difficulty_levels
227
- - labs
228
- - lab_submissions
229
- - lab_instructions
230
- - lab_attendance
231
- - grades
232
- - grade_components
233
- - rubrics
234
- - rubric_criteria
235
- - gpa_calculations
236
-
237
- ### ATTENDANCE (4)
238
- - attendance_sessions
239
- - attendance_records
240
- - attendance_photos
241
- - ai_attendance_processing
242
-
243
- ### COMMUNICATION (5)
244
- - announcements
245
- - course_chat_threads
246
- - chat_messages
247
- - messages
248
- - message_participants
249
-
250
- ### COMMUNITY & FORUMS (6)
251
- - communities
252
- - community_posts
253
- - community_comments
254
- - community_reactions
255
- - community_tags
256
- - community_post_tags (junction)
257
- - forum_categories
258
-
259
- ### FILES & STORAGE (4)
260
- - folders
261
- - files
262
- - file_versions
263
- - file_permissions
264
-
265
- ### NOTIFICATIONS (3)
266
- - notifications
267
- - notification_preferences
268
- - scheduled_notifications
269
-
270
- ### SCHEDULE & CALENDAR (3)
271
- - exam_schedules
272
- - calendar_events
273
- - calendar_integrations
274
-
275
- ### MATERIALS & CONTENT (2)
276
- - course_materials
277
- - lecture_section_lab
278
-
279
- ### TASKS & REMINDERS (3)
280
- - student_tasks
281
- - task_completions
282
- - deadline_reminders
283
-
284
- ### ANALYTICS & REPORTING (7)
285
- - course_analytics
286
- - learning_analytics
287
- - performance_metrics
288
- - student_progress
289
- - weak_topics_analysis
290
- - activity_logs
291
- - generated_reports
292
- - report_templates
293
- - export_history
294
-
295
- ### SEARCH (2)
296
- - search_indexes
297
- - search_history
298
-
299
- ---
300
-
301
- ## Enum Types
302
-
303
- ### User Status
304
- - ACTIVE
305
- - INACTIVE
306
- - SUSPENDED
307
- - PENDING
308
-
309
- ### Enrollment Status
310
- - ENROLLED
311
- - DROPPED
312
- - COMPLETED
313
-
314
- ### Course Status
315
- - ACTIVE
316
- - ARCHIVED
317
-
318
- ### Section Status
319
- - OPEN
320
- - CLOSED
321
- - FULL
322
-
323
- ### Assignment Status
324
- - DRAFT
325
- - PUBLISHED
326
- - CLOSED
327
-
328
- ### Submission Type
329
- - FILE
330
- - TEXT
331
- - CODE
332
-
333
- ### Attendance Status
334
- - PRESENT
335
- - ABSENT
336
- - LATE
337
- - EXCUSED
338
-
339
- ### Quiz Type
340
- - GRADED
341
- - PRACTICE
342
-
343
- ### Question Type
344
- - MULTIPLE_CHOICE
345
- - TRUE_FALSE
346
- - SHORT_ANSWER
347
- - ESSAY
348
-
349
- ### Post Type (Community)
350
- - DISCUSSION
351
- - QUESTION
352
- - ANNOUNCEMENT
353
- - POLL
354
-
355
- ### Notification Priority
356
- - LOW
357
- - MEDIUM
358
- - HIGH
359
- - URGENT
360
-
361
- ### Notification Type
362
- - ANNOUNCEMENT
363
- - GRADE
364
- - ASSIGNMENT
365
- - MESSAGE
366
- - DEADLINE
367
- - SYSTEM
368
-
369
- ### Grade Type
370
- - ASSIGNMENT
371
- - QUIZ
372
- - LAB
373
- - EXAM
374
- - PARTICIPATION
375
-
376
- ### Message Type
377
- - DIRECT
378
- - GROUP
379
- - ANNOUNCEMENT
380
-
381
- ### Campus Status
382
- - ACTIVE
383
- - INACTIVE
384
-
385
- ---
386
-
387
- ## Key Indexes
388
-
389
- ### Performance Indexes
390
- - users(email) - UNIQUE
391
- - courses(departmentId, code) - UNIQUE
392
- - course_sections(courseId, semesterId, sectionNumber) - UNIQUE
393
- - course_enrollments(userId, sectionId) - UNIQUE
394
- - grades(userId, courseId)
395
- - grades(courseId, gradeType)
396
- - attendance_records(sessionId)
397
- - attendance_records(userId)
398
- - attendance_records(attendanceStatus)
399
- - assignments(courseId)
400
- - assignments(createdBy)
401
- - assignments(status)
402
-
403
- ---
404
-
405
- ## Relationship Cardinalities
406
-
407
- ### One-to-One (1:1)
408
- - User ↔ UserPreference
409
- - User ↔ EmailVerification
410
-
411
- ### One-to-Many (1:n)
412
- - Course → CourseSection
413
- - Course → Assignment
414
- - Course → Quiz
415
- - Course → Lab
416
- - User → CourseEnrollment
417
- - CourseEnrollment → Grade
418
-
419
- ### Many-to-Many (n:m)
420
- - User ↔ Role (via user_roles)
421
- - Community ↔ User (members)
422
- - CommunityPost ↔ CommunityTag
423
-
424
- ### Self-Referencing
425
- - Message → ParentMessage/Replies
426
- - CommunityComment → ParentComment/Replies
427
-
428
- ---
429
-
430
- ## Cascade Rules
431
-
432
- ### CASCADE (ON DELETE CASCADE)
433
- - CourseSection → when Course deleted
434
- - Assignment → when Course deleted
435
- - AssignmentSubmission → when Assignment deleted
436
- - Grade → when Course/User/Assignment deleted
437
- - AttendanceRecord → when AttendanceSession deleted
438
- - Quiz → when Course deleted
439
- - Message → when Sender/ParentMessage deleted
440
-
441
- ### SET NULL (ON DELETE SET NULL)
442
- - Grade.gradedBy → when Grader User deleted
443
- - Grade.quiz → when Quiz deleted
444
- - Grade.lab → when Lab deleted
445
- - CourseEnrollment.program → when Program deleted
446
- - PasswordReset → soft delete
447
-
448
- ### RESTRICT (ON DELETE RESTRICT)
449
- - User → cannot delete while enrolled in courses (must drop first)
450
- - Course → cannot delete while having active sections
451
-
452
- ---
453
-
454
- ## Soft Deletes
455
-
456
- The following entities support soft deletion (not physically removed):
457
- - User
458
- - Course
459
- - Assignment
460
- - Quiz
461
- - Announcement
462
- - CommunityPost
463
- - Message
464
-
465
- ### Soft Delete Implementation
466
- - Added deleted_at column
467
- - Type: DeleteDateColumn from TypeORM
468
- - Queries automatically filter deleted records
469
- - Can be restored (set deleted_at to NULL)
470
-
471
- ---
472
-
473
- ## Audit Timestamps
474
-
475
- All main entities include:
476
- - **created_at** - Record creation timestamp (immutable)
477
- - **updated_at** - Last modification timestamp (auto-updated)
478
-
479
- Examples:
480
- - users: createdAt, updatedAt
481
- - courses: createdAt, updatedAt
482
- - grades: createdAt, updatedAt
483
- - messages: sentAt, editedAt
484
-
485
- ---
486
-
487
- ## Junction Tables
488
-
489
- Many-to-Many relationships use junction tables:
490
- - user_roles (user_id ↔ role_id)
491
- - community_post_tags (post_id ↔ tag_id)
492
- - [Any other n:m relationship in future]
493
-
494
- ---
495
-
496
- ## Search Capabilities
497
-
498
- ### Full-Text Search Tables
499
- - search_indexes - Indexed content for quick lookup
500
- - search_history - User search queries for analytics
501
-
502
- ### Searchable Entities
503
- - Courses (name, code, description)
504
- - CourseMaterial (title, content)
505
- - CommunityPost (title, content)
506
- - Announcement (title, content)
507
- - Assignment (title, description, instructions)
508
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
DB_CHANGES_GRADING_FIX.sql ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- =========================================================================
2
+ -- DATABASE MIGRATION: Grading System Fix
3
+ -- =========================================================================
4
+ -- This migration adds missing columns to submission tables and quiz status
5
+ -- to properly integrate all assessment grading with the central grades table.
6
+ --
7
+ -- Run this migration BEFORE deploying the updated backend code.
8
+ -- =========================================================================
9
+
10
+ -- =========================================================================
11
+ -- 1. Add score/feedback columns to assignment_submissions
12
+ -- =========================================================================
13
+ -- These columns persist the grade on the submission record
14
+ -- in addition to the central grades table.
15
+
16
+ -- Check if columns exist before adding
17
+ SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
18
+ WHERE TABLE_SCHEMA = 'eduverse_db' AND TABLE_NAME = 'assignment_submissions' AND COLUMN_NAME = 'score');
19
+
20
+ SET @sql = IF(@col_exists = 0,
21
+ 'ALTER TABLE assignment_submissions
22
+ ADD COLUMN score DECIMAL(5,2) NULL COMMENT ''Score achieved (0-100)'' AFTER submission_link,
23
+ ADD COLUMN feedback TEXT NULL COMMENT ''Grader feedback'' AFTER score,
24
+ ADD COLUMN graded_by BIGINT UNSIGNED NULL COMMENT ''User who graded'' AFTER feedback,
25
+ ADD COLUMN graded_at TIMESTAMP NULL COMMENT ''When graded'' AFTER graded_by',
26
+ 'SELECT ''Columns already exist in assignment_submissions'' AS message');
27
+
28
+ PREPARE stmt FROM @sql;
29
+ EXECUTE stmt;
30
+ DEALLOCATE PREPARE stmt;
31
+
32
+ -- Add foreign key for grader
33
+ ALTER TABLE assignment_submissions
34
+ ADD CONSTRAINT fk_assignment_submission_grader
35
+ FOREIGN KEY (graded_by) REFERENCES users(user_id) ON DELETE SET NULL;
36
+
37
+ -- =========================================================================
38
+ -- 2. Add score/feedback columns to lab_submissions
39
+ -- =========================================================================
40
+ -- Previously lab grading only changed status, now it persists scores.
41
+
42
+ -- Check if columns exist before adding
43
+ SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
44
+ WHERE TABLE_SCHEMA = 'eduverse_db' AND TABLE_NAME = 'lab_submissions' AND COLUMN_NAME = 'score');
45
+
46
+ SET @sql = IF(@col_exists = 0,
47
+ 'ALTER TABLE lab_submissions
48
+ ADD COLUMN score DECIMAL(5,2) NULL COMMENT ''Score achieved (0-100)'' AFTER status,
49
+ ADD COLUMN feedback TEXT NULL COMMENT ''Grader feedback'' AFTER score,
50
+ ADD COLUMN graded_by BIGINT UNSIGNED NULL COMMENT ''User who graded'' AFTER feedback,
51
+ ADD COLUMN graded_at TIMESTAMP NULL COMMENT ''When graded'' AFTER graded_by',
52
+ 'SELECT ''Columns already exist in lab_submissions'' AS message');
53
+
54
+ PREPARE stmt FROM @sql;
55
+ EXECUTE stmt;
56
+ DEALLOCATE PREPARE stmt;
57
+
58
+ -- Add foreign key for grader
59
+ ALTER TABLE lab_submissions
60
+ ADD CONSTRAINT fk_lab_submission_grader
61
+ FOREIGN KEY (graded_by) REFERENCES users(user_id) ON DELETE SET NULL;
62
+
63
+ -- =========================================================================
64
+ -- 3. Add status column to quizzes
65
+ -- =========================================================================
66
+ -- Quizzes now have explicit status like assignments/labs.
67
+
68
+ -- Check if status column exists before adding
69
+ SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
70
+ WHERE TABLE_SCHEMA = 'eduverse_db' AND TABLE_NAME = 'quizzes' AND COLUMN_NAME = 'status');
71
+
72
+ SET @sql = IF(@col_exists = 0,
73
+ 'ALTER TABLE quizzes
74
+ ADD COLUMN status ENUM(''draft'', ''published'', ''closed'', ''archived'')
75
+ NOT NULL DEFAULT ''draft'' COMMENT ''Quiz visibility/availability status'' AFTER description',
76
+ 'SELECT ''Status column already exists in quizzes'' AS message');
77
+
78
+ PREPARE stmt FROM @sql;
79
+ EXECUTE stmt;
80
+ DEALLOCATE PREPARE stmt;
81
+
82
+ -- Set all existing quizzes to published (assuming they were already in use)
83
+ UPDATE quizzes SET status = 'published' WHERE status = 'draft';
84
+
85
+ -- =========================================================================
86
+ -- VERIFICATION QUERIES
87
+ -- =========================================================================
88
+ -- Run these to confirm the changes were applied:
89
+
90
+ -- SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE
91
+ -- FROM INFORMATION_SCHEMA.COLUMNS
92
+ -- WHERE TABLE_NAME = 'assignment_submissions'
93
+ -- AND COLUMN_NAME IN ('score', 'feedback', 'graded_by', 'graded_at');
94
+
95
+ -- SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE
96
+ -- FROM INFORMATION_SCHEMA.COLUMNS
97
+ -- WHERE TABLE_NAME = 'lab_submissions'
98
+ -- AND COLUMN_NAME IN ('score', 'feedback', 'graded_by', 'graded_at');
99
+
100
+ -- SELECT COLUMN_NAME, COLUMN_TYPE
101
+ -- FROM INFORMATION_SCHEMA.COLUMNS
102
+ -- WHERE TABLE_NAME = 'quizzes' AND COLUMN_NAME = 'status';
103
+
104
+ -- =========================================================================
105
+ -- ROLLBACK (if needed)
106
+ -- =========================================================================
107
+ -- ALTER TABLE assignment_submissions DROP FOREIGN KEY fk_assignment_submission_grader;
108
+ -- ALTER TABLE assignment_submissions DROP COLUMN score, DROP COLUMN feedback, DROP COLUMN graded_by, DROP COLUMN graded_at;
109
+
110
+ -- ALTER TABLE lab_submissions DROP FOREIGN KEY fk_lab_submission_grader;
111
+ -- ALTER TABLE lab_submissions DROP COLUMN score, DROP COLUMN feedback, DROP COLUMN graded_by, DROP COLUMN graded_at;
112
+
113
+ -- ALTER TABLE quizzes DROP COLUMN status;
DB_SCHEMA_FIXES.sql DELETED
@@ -1,64 +0,0 @@
1
- -- ============================================================================
2
- -- EduVerse Database Schema Fixes
3
- -- Version: 1.0
4
- -- Created: For fixing entity-database mismatches
5
- -- Description: Contains SQL fixes for schema inconsistencies between
6
- -- TypeORM entities and actual database tables
7
- -- ============================================================================
8
-
9
- -- ============================================================================
10
- -- Fix 1: Add missing columns to courses table
11
- -- ============================================================================
12
- -- The Course entity (src/modules/courses/entities/course.entity.ts) expects
13
- -- these columns but they may be missing from older database schemas.
14
- --
15
- -- Error message this fixes:
16
- -- "QueryFailedError: Unknown column 'course.instructor_id' in 'field list'"
17
- --
18
- -- This error occurs when accessing endpoints that JOIN to the courses table:
19
- -- - GET /api/labs
20
- -- - GET /api/assignments
21
- -- - Any query with relations: ['course']
22
- -- ============================================================================
23
-
24
- -- Check if columns exist before adding (run this to see current state)
25
- -- SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
26
- -- WHERE TABLE_SCHEMA = 'eduverse_db' AND TABLE_NAME = 'courses'
27
- -- AND COLUMN_NAME IN ('instructor_id', 'ta_ids');
28
-
29
- -- Add the missing columns
30
- ALTER TABLE courses
31
- ADD COLUMN instructor_id BIGINT UNSIGNED NULL COMMENT 'Primary instructor user ID',
32
- ADD COLUMN ta_ids JSON NULL COMMENT 'Array of TA user IDs';
33
-
34
- -- Note: If columns already exist, you'll get an error. That's expected.
35
- -- You can safely ignore "Duplicate column name" errors.
36
-
37
- -- ============================================================================
38
- -- Verification Queries
39
- -- ============================================================================
40
-
41
- -- Verify the columns were added
42
- SHOW COLUMNS FROM courses WHERE Field IN ('instructor_id', 'ta_ids');
43
-
44
- -- Test that labs endpoint query works
45
- SELECT l.*, c.course_code, c.course_name
46
- FROM labs l
47
- LEFT JOIN courses c ON l.course_id = c.course_id
48
- LIMIT 1;
49
-
50
- -- Test that assignments endpoint query works
51
- SELECT a.*, c.course_code, c.course_name
52
- FROM assignments a
53
- LEFT JOIN courses c ON a.course_id = c.course_id
54
- LIMIT 1;
55
-
56
- -- ============================================================================
57
- -- Rollback (if needed)
58
- -- ============================================================================
59
- -- WARNING: Only run this if you need to undo the changes
60
- --
61
- -- ALTER TABLE courses
62
- -- DROP COLUMN instructor_id,
63
- -- DROP COLUMN ta_ids;
64
- -- ============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Documentation/ASSESSMENT_INTEGRATION_GUIDE.md ADDED
@@ -0,0 +1,846 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Assessment Integration Guide
2
+
3
+ > Common patterns and shared components for integrating Quizzes, Assignments, and Labs in the EduVerse frontend.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Overview](#overview)
10
+ 2. [Authentication & Authorization](#authentication--authorization)
11
+ 3. [Common API Patterns](#common-api-patterns)
12
+ 4. [File Upload Integration](#file-upload-integration)
13
+ 5. [Grading Integration](#grading-integration)
14
+ 6. [State Management](#state-management)
15
+ 7. [Shared Components](#shared-components)
16
+ 8. [Error Handling](#error-handling)
17
+ 9. [Real-time Features](#real-time-features)
18
+
19
+ ---
20
+
21
+ ## Overview
22
+
23
+ All three assessment modules (Quizzes, Assignments, Labs) share common patterns:
24
+
25
+ | Feature | Quizzes | Assignments | Labs |
26
+ |---------|---------|-------------|------|
27
+ | Status workflow | ✅ draft→published→closed→archived | ✅ | ✅ |
28
+ | File attachments | ❌ (planned) | ✅ Google Drive | ✅ Google Drive |
29
+ | Auto-grading | ✅ MCQ/TF | ❌ | ❌ |
30
+ | Manual grading | ✅ Essay | ✅ | ✅ |
31
+ | Grades table integration | ✅ | ✅ | ✅ |
32
+ | Due dates | ✅ availableUntil | ✅ dueDate | ✅ dueDate |
33
+ | Late handling | ❌ | ✅ latePenalty % | ✅ isLate flag |
34
+ | Attendance | ❌ | ❌ | ✅ |
35
+
36
+ ---
37
+
38
+ ## Authentication & Authorization
39
+
40
+ ### JWT Token Usage
41
+
42
+ All API requests require a Bearer token:
43
+
44
+ ```typescript
45
+ // api/client.ts
46
+ const API_BASE = 'http://localhost:8081';
47
+
48
+ export async function apiRequest<T>(
49
+ endpoint: string,
50
+ options: RequestInit = {}
51
+ ): Promise<T> {
52
+ const token = localStorage.getItem('accessToken');
53
+
54
+ const response = await fetch(`${API_BASE}${endpoint}`, {
55
+ ...options,
56
+ headers: {
57
+ 'Content-Type': 'application/json',
58
+ 'Authorization': `Bearer ${token}`,
59
+ ...options.headers,
60
+ },
61
+ });
62
+
63
+ if (response.status === 401) {
64
+ // Token expired - redirect to login
65
+ localStorage.removeItem('accessToken');
66
+ window.location.href = '/login';
67
+ throw new Error('Unauthorized');
68
+ }
69
+
70
+ if (!response.ok) {
71
+ const error = await response.json();
72
+ throw new Error(error.message || 'Request failed');
73
+ }
74
+
75
+ return response.json();
76
+ }
77
+ ```
78
+
79
+ ### Login Flow
80
+
81
+ ```typescript
82
+ // POST /api/auth/login
83
+ interface LoginDto {
84
+ email: string;
85
+ password: string;
86
+ rememberMe?: boolean;
87
+ }
88
+
89
+ interface LoginResponse {
90
+ accessToken: string;
91
+ }
92
+
93
+ const login = async (credentials: LoginDto): Promise<void> => {
94
+ const response = await fetch('/api/auth/login', {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify(credentials),
98
+ });
99
+
100
+ if (!response.ok) {
101
+ throw new Error('Invalid credentials');
102
+ }
103
+
104
+ const { accessToken } = await response.json();
105
+ localStorage.setItem('accessToken', accessToken);
106
+ };
107
+ ```
108
+
109
+ ### Role-Based UI
110
+
111
+ ```typescript
112
+ // hooks/useAuth.ts
113
+ interface User {
114
+ userId: number;
115
+ email: string;
116
+ firstName: string;
117
+ lastName: string;
118
+ roles: Array<{ roleName: string }>;
119
+ }
120
+
121
+ export function useAuth() {
122
+ const [user, setUser] = useState<User | null>(null);
123
+
124
+ const hasRole = (role: string) => {
125
+ return user?.roles.some(r => r.roleName === role) ?? false;
126
+ };
127
+
128
+ const isInstructor = hasRole('instructor');
129
+ const isStudent = hasRole('student');
130
+ const isTA = hasRole('teaching_assistant');
131
+ const isAdmin = hasRole('admin') || hasRole('it_admin');
132
+
133
+ const canManageAssessments = isInstructor || isTA || isAdmin;
134
+ const canGrade = isInstructor || isTA;
135
+ const canSubmit = isStudent;
136
+
137
+ return { user, hasRole, isInstructor, isStudent, isTA, canManageAssessments, canGrade, canSubmit };
138
+ }
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Common API Patterns
144
+
145
+ ### Pagination
146
+
147
+ All list endpoints support pagination:
148
+
149
+ ```typescript
150
+ interface PaginatedResponse<T> {
151
+ data: T[];
152
+ meta: {
153
+ total: number;
154
+ page: number;
155
+ limit: number;
156
+ totalPages: number;
157
+ };
158
+ }
159
+
160
+ // Usage
161
+ const getQuizzes = async (page = 1, limit = 10): Promise<PaginatedResponse<Quiz>> => {
162
+ return apiRequest(`/api/quizzes?page=${page}&limit=${limit}`);
163
+ };
164
+ ```
165
+
166
+ ### Filtering
167
+
168
+ Common query parameters:
169
+
170
+ | Parameter | Description | Example |
171
+ |-----------|-------------|---------|
172
+ | `courseId` | Filter by course | `?courseId=1` |
173
+ | `status` | Filter by status | `?status=published` |
174
+ | `page` | Page number | `?page=2` |
175
+ | `limit` | Items per page | `?limit=20` |
176
+ | `search` | Search text | `?search=midterm` |
177
+
178
+ ### Status Change Pattern
179
+
180
+ All modules use the same status change pattern:
181
+
182
+ ```typescript
183
+ type AssessmentStatus = 'draft' | 'published' | 'closed' | 'archived';
184
+
185
+ const changeStatus = async (
186
+ type: 'quizzes' | 'assignments' | 'labs',
187
+ id: number,
188
+ status: AssessmentStatus
189
+ ): Promise<void> => {
190
+ await apiRequest(`/api/${type}/${id}/status`, {
191
+ method: 'PATCH',
192
+ body: JSON.stringify({ status }),
193
+ });
194
+ };
195
+ ```
196
+
197
+ ---
198
+
199
+ ## File Upload Integration
200
+
201
+ ### Google Drive Integration
202
+
203
+ The backend uses Google Drive for file storage. Two-step upload process:
204
+
205
+ ```typescript
206
+ // Step 1: Upload file to Drive
207
+ interface UploadResponse {
208
+ fileId: number;
209
+ fileName: string;
210
+ webViewLink: string;
211
+ }
212
+
213
+ const uploadFile = async (
214
+ type: 'assignments' | 'labs',
215
+ id: number,
216
+ file: File,
217
+ purpose: 'instructions' | 'submissions' | 'ta-materials'
218
+ ): Promise<UploadResponse> => {
219
+ const formData = new FormData();
220
+ formData.append('file', file);
221
+
222
+ const endpoint = purpose === 'instructions'
223
+ ? `/api/${type}/${id}/instructions/upload`
224
+ : purpose === 'ta-materials'
225
+ ? `/api/${type}/${id}/ta-materials/upload`
226
+ : `/api/${type}/${id}/submissions/upload`;
227
+
228
+ const response = await fetch(endpoint, {
229
+ method: 'POST',
230
+ headers: {
231
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
232
+ },
233
+ body: formData,
234
+ });
235
+
236
+ return response.json();
237
+ };
238
+
239
+ // Step 2: Submit with file ID
240
+ const submitWithFile = async (
241
+ type: 'assignments' | 'labs',
242
+ id: number,
243
+ fileId: number
244
+ ): Promise<void> => {
245
+ await apiRequest(`/api/${type}/${id}/submit`, {
246
+ method: 'POST',
247
+ body: JSON.stringify({ fileId }),
248
+ });
249
+ };
250
+ ```
251
+
252
+ ### File Upload Component
253
+
254
+ ```typescript
255
+ // components/FileUploader.tsx
256
+ import { useState, useCallback } from 'react';
257
+
258
+ interface FileUploaderProps {
259
+ type: 'assignments' | 'labs';
260
+ id: number;
261
+ purpose: 'instructions' | 'submissions' | 'ta-materials';
262
+ accept?: string;
263
+ maxSize?: number; // bytes
264
+ onUpload: (fileId: number, fileName: string) => void;
265
+ onError: (error: string) => void;
266
+ }
267
+
268
+ export function FileUploader({
269
+ type,
270
+ id,
271
+ purpose,
272
+ accept = '*/*',
273
+ maxSize = 10 * 1024 * 1024, // 10MB default
274
+ onUpload,
275
+ onError,
276
+ }: FileUploaderProps) {
277
+ const [uploading, setUploading] = useState(false);
278
+ const [progress, setProgress] = useState(0);
279
+
280
+ const handleFile = useCallback(async (file: File) => {
281
+ // Validate size
282
+ if (file.size > maxSize) {
283
+ onError(`File too large. Max size: ${formatBytes(maxSize)}`);
284
+ return;
285
+ }
286
+
287
+ setUploading(true);
288
+ setProgress(0);
289
+
290
+ try {
291
+ const result = await uploadFile(type, id, file, purpose);
292
+ onUpload(result.fileId, result.fileName);
293
+ } catch (err) {
294
+ onError(err instanceof Error ? err.message : 'Upload failed');
295
+ } finally {
296
+ setUploading(false);
297
+ }
298
+ }, [type, id, purpose, maxSize, onUpload, onError]);
299
+
300
+ return (
301
+ <div className="file-uploader">
302
+ <input
303
+ type="file"
304
+ accept={accept}
305
+ disabled={uploading}
306
+ onChange={(e) => e.target.files?.[0] && handleFile(e.target.files[0])}
307
+ />
308
+ {uploading && (
309
+ <div className="progress-bar">
310
+ <div className="progress" style={{ width: `${progress}%` }} />
311
+ </div>
312
+ )}
313
+ </div>
314
+ );
315
+ }
316
+
317
+ function formatBytes(bytes: number): string {
318
+ if (bytes < 1024) return `${bytes} B`;
319
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
320
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
321
+ }
322
+ ```
323
+
324
+ ---
325
+
326
+ ## Grading Integration
327
+
328
+ ### Central Grades Table
329
+
330
+ All grades are now stored in the central `grades` table. When you grade a submission, the backend automatically:
331
+
332
+ 1. Saves score/feedback to the submission record
333
+ 2. Creates/updates a `Grade` record in the grades table
334
+ 3. Calculates percentage and letter grade
335
+
336
+ ```typescript
337
+ // Grade is automatically created when grading:
338
+ // - Quiz: After auto-grading or manual grading
339
+ // - Assignment: When calling PATCH /grade
340
+ // - Lab: When calling PATCH /grade
341
+
342
+ interface Grade {
343
+ gradeId: number;
344
+ userId: number;
345
+ courseId: number;
346
+ gradeType: 'assignment' | 'quiz' | 'lab' | 'exam' | 'final';
347
+ assignmentId?: number;
348
+ quizId?: number;
349
+ labId?: number;
350
+ score: number;
351
+ maxScore: number;
352
+ percentage: number;
353
+ letterGrade: string;
354
+ feedback?: string;
355
+ isPublished: boolean;
356
+ createdAt: Date;
357
+ updatedAt: Date;
358
+ }
359
+ ```
360
+
361
+ ### Viewing Grades
362
+
363
+ ```typescript
364
+ // GET /api/grades/my - Student's grades
365
+ const getMyGrades = async (): Promise<PaginatedResponse<Grade>> => {
366
+ return apiRequest('/api/grades/my');
367
+ };
368
+
369
+ // GET /api/grades?courseId=X - Course grades (instructor)
370
+ const getCourseGrades = async (courseId: number): Promise<PaginatedResponse<Grade>> => {
371
+ return apiRequest(`/api/grades?courseId=${courseId}`);
372
+ };
373
+
374
+ // GET /api/grades/transcript/:studentId - Student transcript
375
+ const getTranscript = async (studentId: number): Promise<Transcript> => {
376
+ return apiRequest(`/api/grades/transcript/${studentId}`);
377
+ };
378
+
379
+ // GET /api/grades/gpa/:studentId - GPA calculation
380
+ const getGPA = async (studentId: number): Promise<GpaResult> => {
381
+ return apiRequest(`/api/grades/gpa/${studentId}`);
382
+ };
383
+ ```
384
+
385
+ ---
386
+
387
+ ## State Management
388
+
389
+ ### Recommended Store Structure (Zustand)
390
+
391
+ ```typescript
392
+ // stores/assessmentStore.ts
393
+ import { create } from 'zustand';
394
+
395
+ interface AssessmentStore {
396
+ // Quizzes
397
+ quizzes: Quiz[];
398
+ loadingQuizzes: boolean;
399
+ fetchQuizzes: (courseId?: number) => Promise<void>;
400
+
401
+ // Assignments
402
+ assignments: Assignment[];
403
+ loadingAssignments: boolean;
404
+ fetchAssignments: (courseId?: number) => Promise<void>;
405
+
406
+ // Labs
407
+ labs: Lab[];
408
+ loadingLabs: boolean;
409
+ fetchLabs: (courseId?: number) => Promise<void>;
410
+
411
+ // Common
412
+ currentCourseId: number | null;
413
+ setCourseId: (id: number) => void;
414
+ }
415
+
416
+ export const useAssessmentStore = create<AssessmentStore>((set, get) => ({
417
+ // Quizzes
418
+ quizzes: [],
419
+ loadingQuizzes: false,
420
+ fetchQuizzes: async (courseId) => {
421
+ set({ loadingQuizzes: true });
422
+ const cid = courseId || get().currentCourseId;
423
+ const { data } = await apiRequest<PaginatedResponse<Quiz>>(
424
+ `/api/quizzes${cid ? `?courseId=${cid}` : ''}`
425
+ );
426
+ set({ quizzes: data, loadingQuizzes: false });
427
+ },
428
+
429
+ // Assignments
430
+ assignments: [],
431
+ loadingAssignments: false,
432
+ fetchAssignments: async (courseId) => {
433
+ set({ loadingAssignments: true });
434
+ const cid = courseId || get().currentCourseId;
435
+ const { data } = await apiRequest<PaginatedResponse<Assignment>>(
436
+ `/api/assignments${cid ? `?courseId=${cid}` : ''}`
437
+ );
438
+ set({ assignments: data, loadingAssignments: false });
439
+ },
440
+
441
+ // Labs
442
+ labs: [],
443
+ loadingLabs: false,
444
+ fetchLabs: async (courseId) => {
445
+ set({ loadingLabs: true });
446
+ const cid = courseId || get().currentCourseId;
447
+ const { data } = await apiRequest<PaginatedResponse<Lab>>(
448
+ `/api/labs${cid ? `?courseId=${cid}` : ''}`
449
+ );
450
+ set({ labs: data, loadingLabs: false });
451
+ },
452
+
453
+ // Common
454
+ currentCourseId: null,
455
+ setCourseId: (id) => set({ currentCourseId: id }),
456
+ }));
457
+ ```
458
+
459
+ ### Caching Strategy
460
+
461
+ ```typescript
462
+ // hooks/useAssessmentCache.ts
463
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
464
+
465
+ // Quizzes
466
+ export function useQuizzes(courseId?: number) {
467
+ return useQuery({
468
+ queryKey: ['quizzes', courseId],
469
+ queryFn: () => apiRequest<PaginatedResponse<Quiz>>(
470
+ `/api/quizzes${courseId ? `?courseId=${courseId}` : ''}`
471
+ ),
472
+ staleTime: 5 * 60 * 1000, // 5 minutes
473
+ });
474
+ }
475
+
476
+ // Single Quiz
477
+ export function useQuiz(id: number) {
478
+ return useQuery({
479
+ queryKey: ['quiz', id],
480
+ queryFn: () => apiRequest<Quiz>(`/api/quizzes/${id}`),
481
+ });
482
+ }
483
+
484
+ // Create Quiz
485
+ export function useCreateQuiz() {
486
+ const queryClient = useQueryClient();
487
+
488
+ return useMutation({
489
+ mutationFn: (data: CreateQuizDto) =>
490
+ apiRequest<Quiz>('/api/quizzes', {
491
+ method: 'POST',
492
+ body: JSON.stringify(data),
493
+ }),
494
+ onSuccess: () => {
495
+ queryClient.invalidateQueries({ queryKey: ['quizzes'] });
496
+ },
497
+ });
498
+ }
499
+
500
+ // Update Quiz Status
501
+ export function useChangeQuizStatus() {
502
+ const queryClient = useQueryClient();
503
+
504
+ return useMutation({
505
+ mutationFn: ({ id, status }: { id: number; status: string }) =>
506
+ apiRequest<Quiz>(`/api/quizzes/${id}/status`, {
507
+ method: 'PATCH',
508
+ body: JSON.stringify({ status }),
509
+ }),
510
+ onSuccess: (_, { id }) => {
511
+ queryClient.invalidateQueries({ queryKey: ['quiz', id] });
512
+ queryClient.invalidateQueries({ queryKey: ['quizzes'] });
513
+ },
514
+ });
515
+ }
516
+ ```
517
+
518
+ ---
519
+
520
+ ## Shared Components
521
+
522
+ ### Status Badge
523
+
524
+ ```typescript
525
+ // components/StatusBadge.tsx
526
+ type Status = 'draft' | 'published' | 'closed' | 'archived';
527
+
528
+ const statusColors: Record<Status, string> = {
529
+ draft: 'bg-gray-500',
530
+ published: 'bg-green-500',
531
+ closed: 'bg-yellow-500',
532
+ archived: 'bg-red-500',
533
+ };
534
+
535
+ export function StatusBadge({ status }: { status: Status }) {
536
+ return (
537
+ <span className={`px-2 py-1 rounded text-white text-sm ${statusColors[status]}`}>
538
+ {status.charAt(0).toUpperCase() + status.slice(1)}
539
+ </span>
540
+ );
541
+ }
542
+ ```
543
+
544
+ ### Due Date Display
545
+
546
+ ```typescript
547
+ // components/DueDate.tsx
548
+ import { formatDistanceToNow, isPast, isToday, isTomorrow } from 'date-fns';
549
+
550
+ interface DueDateProps {
551
+ date: Date;
552
+ showCountdown?: boolean;
553
+ }
554
+
555
+ export function DueDate({ date, showCountdown = true }: DueDateProps) {
556
+ const due = new Date(date);
557
+ const overdue = isPast(due);
558
+ const today = isToday(due);
559
+ const tomorrow = isTomorrow(due);
560
+
561
+ let label: string;
562
+ let className: string;
563
+
564
+ if (overdue) {
565
+ label = 'Overdue';
566
+ className = 'text-red-600 font-bold';
567
+ } else if (today) {
568
+ label = 'Due Today';
569
+ className = 'text-orange-600 font-bold';
570
+ } else if (tomorrow) {
571
+ label = 'Due Tomorrow';
572
+ className = 'text-yellow-600';
573
+ } else {
574
+ label = showCountdown
575
+ ? `Due ${formatDistanceToNow(due, { addSuffix: true })}`
576
+ : due.toLocaleDateString();
577
+ className = 'text-gray-600';
578
+ }
579
+
580
+ return (
581
+ <span className={className} title={due.toLocaleString()}>
582
+ {label}
583
+ </span>
584
+ );
585
+ }
586
+ ```
587
+
588
+ ### Grade Display
589
+
590
+ ```typescript
591
+ // components/GradeDisplay.tsx
592
+ interface GradeDisplayProps {
593
+ score: number;
594
+ maxScore: number;
595
+ percentage?: number;
596
+ letterGrade?: string;
597
+ showLetter?: boolean;
598
+ }
599
+
600
+ export function GradeDisplay({
601
+ score,
602
+ maxScore,
603
+ percentage,
604
+ letterGrade,
605
+ showLetter = true,
606
+ }: GradeDisplayProps) {
607
+ const pct = percentage ?? (score / maxScore) * 100;
608
+ const letter = letterGrade ?? getLetterGrade(pct);
609
+
610
+ const getColor = () => {
611
+ if (pct >= 90) return 'text-green-600';
612
+ if (pct >= 80) return 'text-blue-600';
613
+ if (pct >= 70) return 'text-yellow-600';
614
+ if (pct >= 60) return 'text-orange-600';
615
+ return 'text-red-600';
616
+ };
617
+
618
+ return (
619
+ <div className={`grade-display ${getColor()}`}>
620
+ <span className="score">{score} / {maxScore}</span>
621
+ <span className="percentage">({pct.toFixed(1)}%)</span>
622
+ {showLetter && <span className="letter">{letter}</span>}
623
+ </div>
624
+ );
625
+ }
626
+
627
+ function getLetterGrade(percentage: number): string {
628
+ if (percentage >= 97) return 'A+';
629
+ if (percentage >= 93) return 'A';
630
+ if (percentage >= 90) return 'A-';
631
+ if (percentage >= 87) return 'B+';
632
+ if (percentage >= 83) return 'B';
633
+ if (percentage >= 80) return 'B-';
634
+ if (percentage >= 77) return 'C+';
635
+ if (percentage >= 73) return 'C';
636
+ if (percentage >= 70) return 'C-';
637
+ if (percentage >= 67) return 'D+';
638
+ if (percentage >= 63) return 'D';
639
+ if (percentage >= 60) return 'D-';
640
+ return 'F';
641
+ }
642
+ ```
643
+
644
+ ### Loading Skeleton
645
+
646
+ ```typescript
647
+ // components/AssessmentSkeleton.tsx
648
+ export function AssessmentCardSkeleton() {
649
+ return (
650
+ <div className="assessment-card skeleton">
651
+ <div className="skeleton-line title" />
652
+ <div className="skeleton-line description" />
653
+ <div className="skeleton-line meta" />
654
+ </div>
655
+ );
656
+ }
657
+
658
+ export function AssessmentListSkeleton({ count = 5 }: { count?: number }) {
659
+ return (
660
+ <div className="assessment-list">
661
+ {Array.from({ length: count }).map((_, i) => (
662
+ <AssessmentCardSkeleton key={i} />
663
+ ))}
664
+ </div>
665
+ );
666
+ }
667
+ ```
668
+
669
+ ---
670
+
671
+ ## Error Handling
672
+
673
+ ### Common Error Codes
674
+
675
+ | Status | Meaning | Action |
676
+ |--------|---------|--------|
677
+ | 400 | Bad Request | Show validation errors |
678
+ | 401 | Unauthorized | Redirect to login |
679
+ | 403 | Forbidden | Show "access denied" |
680
+ | 404 | Not Found | Show "not found" page |
681
+ | 409 | Conflict | Show conflict message |
682
+ | 500 | Server Error | Show generic error |
683
+
684
+ ### Error Boundary
685
+
686
+ ```typescript
687
+ // components/ErrorBoundary.tsx
688
+ import { Component, ErrorInfo, ReactNode } from 'react';
689
+
690
+ interface Props {
691
+ children: ReactNode;
692
+ fallback?: ReactNode;
693
+ }
694
+
695
+ interface State {
696
+ hasError: boolean;
697
+ error?: Error;
698
+ }
699
+
700
+ export class ErrorBoundary extends Component<Props, State> {
701
+ state: State = { hasError: false };
702
+
703
+ static getDerivedStateFromError(error: Error): State {
704
+ return { hasError: true, error };
705
+ }
706
+
707
+ componentDidCatch(error: Error, info: ErrorInfo) {
708
+ console.error('Error caught:', error, info);
709
+ // Log to monitoring service
710
+ }
711
+
712
+ render() {
713
+ if (this.state.hasError) {
714
+ return this.props.fallback || (
715
+ <div className="error-fallback">
716
+ <h2>Something went wrong</h2>
717
+ <p>{this.state.error?.message}</p>
718
+ <button onClick={() => window.location.reload()}>
719
+ Refresh Page
720
+ </button>
721
+ </div>
722
+ );
723
+ }
724
+
725
+ return this.props.children;
726
+ }
727
+ }
728
+ ```
729
+
730
+ ### API Error Hook
731
+
732
+ ```typescript
733
+ // hooks/useApiError.ts
734
+ import { useState, useCallback } from 'react';
735
+
736
+ interface ApiError {
737
+ message: string;
738
+ statusCode: number;
739
+ field?: string;
740
+ }
741
+
742
+ export function useApiError() {
743
+ const [error, setError] = useState<ApiError | null>(null);
744
+
745
+ const handleError = useCallback((err: unknown) => {
746
+ if (err instanceof Response) {
747
+ err.json().then(data => {
748
+ setError({
749
+ message: data.message || 'An error occurred',
750
+ statusCode: err.status,
751
+ field: data.field,
752
+ });
753
+ });
754
+ } else if (err instanceof Error) {
755
+ setError({
756
+ message: err.message,
757
+ statusCode: 500,
758
+ });
759
+ }
760
+ }, []);
761
+
762
+ const clearError = useCallback(() => setError(null), []);
763
+
764
+ return { error, handleError, clearError };
765
+ }
766
+ ```
767
+
768
+ ---
769
+
770
+ ## Real-time Features
771
+
772
+ ### Quiz Timer WebSocket (Future)
773
+
774
+ ```typescript
775
+ // For quiz timers, consider WebSocket connection for:
776
+ // - Server-authoritative timer
777
+ // - Auto-submit on timeout
778
+ // - Sync across devices
779
+
780
+ interface TimerMessage {
781
+ type: 'TICK' | 'EXPIRED' | 'SYNC';
782
+ attemptId: number;
783
+ remainingSeconds: number;
784
+ }
785
+
786
+ // Example WebSocket setup (to be implemented)
787
+ const connectQuizTimer = (attemptId: number, onMessage: (msg: TimerMessage) => void) => {
788
+ const ws = new WebSocket(`ws://localhost:8081/quiz-timer/${attemptId}`);
789
+
790
+ ws.onmessage = (event) => {
791
+ const message = JSON.parse(event.data);
792
+ onMessage(message);
793
+ };
794
+
795
+ return () => ws.close();
796
+ };
797
+ ```
798
+
799
+ ### Notifications
800
+
801
+ ```typescript
802
+ // Poll for notifications (WebSocket integration planned)
803
+ const useNotifications = () => {
804
+ const [notifications, setNotifications] = useState<Notification[]>([]);
805
+
806
+ useEffect(() => {
807
+ const poll = setInterval(async () => {
808
+ const data = await apiRequest<Notification[]>('/api/notifications/unread');
809
+ setNotifications(data);
810
+ }, 30000); // Poll every 30 seconds
811
+
812
+ return () => clearInterval(poll);
813
+ }, []);
814
+
815
+ return notifications;
816
+ };
817
+ ```
818
+
819
+ ---
820
+
821
+ ## Quick Reference
822
+
823
+ ### Endpoint Summary
824
+
825
+ | Module | Base URL | CRUD | Submit | Grade | Status |
826
+ |--------|----------|------|--------|-------|--------|
827
+ | Quizzes | `/api/quizzes` | ✅ | POST `/attempts/start` | POST `/attempts/:id/grade` | PATCH `/:id/status` |
828
+ | Assignments | `/api/assignments` | ✅ | POST `/:id/submit` | PATCH `/:id/submissions/:subId/grade` | PATCH `/:id/status` |
829
+ | Labs | `/api/labs` | ✅ | POST `/:id/submit` | PATCH `/:id/submissions/:subId/grade` | PATCH `/:id/status` |
830
+
831
+ ### Status Values
832
+
833
+ All three modules use the same status values:
834
+ - `draft` - Not visible to students
835
+ - `published` - Available to students
836
+ - `closed` - No longer accepting submissions
837
+ - `archived` - Hidden from all views
838
+
839
+ ### Test Accounts
840
+
841
+ | Role | Email | Password |
842
+ |------|-------|----------|
843
+ | Admin | admin.tarek@example.com | SecureP@ss123 |
844
+ | Instructor | instructor.tarek@example.com | SecureP@ss123 |
845
+ | TA | ta.tarek@example.com | SecureP@ss123 |
846
+ | Student | student.tarek@example.com | SecureP@ss123 |
Documentation/ASSIGNMENTS_FRONTEND_GUIDE.md ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Assignments Frontend Integration Guide
2
+
3
+ > Complete API reference and integration guide for building assignment functionality in the EduVerse frontend.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Overview](#overview)
10
+ 2. [User Stories](#user-stories)
11
+ 3. [API Endpoints](#api-endpoints)
12
+ 4. [Data Models](#data-models)
13
+ 5. [Flow Diagrams](#flow-diagrams)
14
+ 6. [Component Structure](#component-structure)
15
+ 7. [Code Examples](#code-examples)
16
+
17
+ ---
18
+
19
+ ## Overview
20
+
21
+ The Assignments module supports:
22
+ - **Submission Types**: Text, file upload, link, or Google Drive file
23
+ - **File Management**: Local upload or Google Drive integration
24
+ - **Late Submissions**: Configurable late penalty percentage
25
+ - **Status**: draft → published → closed → archived
26
+ - **Grading**: Manual grading with score and feedback
27
+
28
+ ---
29
+
30
+ ## User Stories
31
+
32
+ ### Instructor
33
+
34
+ | Story | Description |
35
+ |-------|-------------|
36
+ | **Create Assignment** | Create assignment with title, description, due date, max score |
37
+ | **Add Instructions** | Upload instruction files to Google Drive |
38
+ | **Set Requirements** | Configure submission type, file limits, late penalty |
39
+ | **Publish** | Change status to make visible to students |
40
+ | **View Submissions** | See all student submissions with timestamps |
41
+ | **Grade** | Assign score and feedback to submissions |
42
+ | **Download Files** | Download/view student submitted files |
43
+
44
+ ### Student
45
+
46
+ | Story | Description |
47
+ |-------|-------------|
48
+ | **View Assignments** | See all assignments for enrolled courses |
49
+ | **View Details** | Read instructions and requirements |
50
+ | **Submit** | Submit text, upload file, paste link, or select Drive file |
51
+ | **Check Status** | See if submission is on time or late |
52
+ | **View Grade** | See score and feedback after grading |
53
+ | **Resubmit** | Submit again if allowed (increments attempt number) |
54
+
55
+ ---
56
+
57
+ ## API Endpoints
58
+
59
+ ### Assignment CRUD
60
+
61
+ | Method | Endpoint | Description | Roles |
62
+ |--------|----------|-------------|-------|
63
+ | `GET` | `/api/assignments` | List assignments (with filters) | All authenticated |
64
+ | `GET` | `/api/assignments/:id` | Get assignment details | All authenticated |
65
+ | `POST` | `/api/assignments` | Create assignment | INSTRUCTOR, TA, ADMIN |
66
+ | `PATCH` | `/api/assignments/:id` | Update assignment | INSTRUCTOR, TA, ADMIN |
67
+ | `DELETE` | `/api/assignments/:id` | Delete assignment | INSTRUCTOR, ADMIN |
68
+ | `PATCH` | `/api/assignments/:id/status` | Change status | INSTRUCTOR, TA, ADMIN |
69
+
70
+ ### Instructions (Google Drive)
71
+
72
+ | Method | Endpoint | Description | Roles |
73
+ |--------|----------|-------------|-------|
74
+ | `POST` | `/api/assignments/:id/instructions/upload` | Upload instruction file | INSTRUCTOR, TA, ADMIN |
75
+
76
+ ### Submissions
77
+
78
+ | Method | Endpoint | Description | Roles |
79
+ |--------|----------|-------------|-------|
80
+ | `GET` | `/api/assignments/:id/submissions` | List all submissions | INSTRUCTOR, TA |
81
+ | `GET` | `/api/assignments/:id/submissions/my` | Get my submission | STUDENT |
82
+ | `POST` | `/api/assignments/:id/submit` | Submit assignment | STUDENT |
83
+ | `POST` | `/api/assignments/:id/submissions/upload` | Upload file to Drive | STUDENT |
84
+
85
+ ### Grading
86
+
87
+ | Method | Endpoint | Description | Roles |
88
+ |--------|----------|-------------|-------|
89
+ | `PATCH` | `/api/assignments/:id/submissions/:subId/grade` | Grade submission | INSTRUCTOR, TA |
90
+
91
+ ---
92
+
93
+ ## Data Models
94
+
95
+ ### Assignment
96
+
97
+ ```typescript
98
+ interface Assignment {
99
+ assignmentId: number;
100
+ courseId: number;
101
+ title: string;
102
+ description?: string;
103
+ instructions?: string; // Markdown text
104
+ dueDate?: Date;
105
+ maxScore: number;
106
+ weight?: number;
107
+ status: 'draft' | 'published' | 'closed' | 'archived';
108
+ submissionType: 'text' | 'file' | 'link' | 'any';
109
+ maxFileSize?: number; // in bytes
110
+ allowedFileTypes?: string[]; // e.g., ['pdf', 'docx']
111
+ latePenalty?: number; // percentage per day
112
+ createdAt: Date;
113
+ updatedAt: Date;
114
+ }
115
+ ```
116
+
117
+ ### AssignmentSubmission
118
+
119
+ ```typescript
120
+ interface AssignmentSubmission {
121
+ submissionId: number;
122
+ assignmentId: number;
123
+ userId: number;
124
+ user?: User;
125
+ submissionText?: string;
126
+ submissionLink?: string;
127
+ fileId?: number; // Drive file ID
128
+ file?: DriveFile;
129
+ score?: number;
130
+ feedback?: string;
131
+ gradedBy?: number;
132
+ gradedAt?: Date;
133
+ isLate: boolean;
134
+ attemptNumber: number;
135
+ submittedAt: Date;
136
+ }
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Flow Diagrams
142
+
143
+ ### Assignment Submission Flow
144
+
145
+ ```
146
+ ┌─────────────────┐
147
+ │ Student Views │
148
+ │ Assignment │
149
+ └────────┬────────┘
150
+
151
+
152
+ ┌─────────────────┐
153
+ │ Read Instructions│
154
+ │ & Requirements │
155
+ └────────┬────────┘
156
+
157
+
158
+ ┌─────────────────────────────────────┐
159
+ │ Choose Submission Type: │
160
+ │ ┌─────┐ ┌─────┐ ┌─────┐ ���─────────┐│
161
+ │ │Text │ │File │ │Link │ │Drive ││
162
+ │ └──┬──┘ └──┬──┘ └──┬──┘ └────┬────┘│
163
+ └────┼───────┼───────┼─────────┼─────┘
164
+ │ │ │ │
165
+ └───────┴───────┴─────────┘
166
+
167
+
168
+ ┌─────────────────┐
169
+ │ POST /submit │
170
+ │ (with content) │
171
+ └────────┬────────┘
172
+
173
+
174
+ ┌─────────────────┐
175
+ │ Check Due Date │
176
+ │ → isLate flag │
177
+ └────────┬────────┘
178
+
179
+
180
+ ┌─────────────────┐
181
+ │ Submission │
182
+ │ Created │
183
+ └────────┬────────┘
184
+
185
+ Wait for Grading
186
+
187
+
188
+ ┌─────────────────┐
189
+ │ View Grade & │
190
+ │ Feedback │
191
+ └─────────────────┘
192
+ ```
193
+
194
+ ### Assignment Creation Flow
195
+
196
+ ```
197
+ ┌─────────────────┐
198
+ │ Instructor │
199
+ │ Creates │
200
+ └────────┬────────┘
201
+
202
+
203
+ ┌─────────────────┐
204
+ │ POST /assignments│
205
+ │ (status: draft) │
206
+ └────────┬────────┘
207
+
208
+
209
+ ┌─────────────────┐
210
+ │ Upload Instruction│
211
+ │ Files (optional)│
212
+ └────────┬────────┘
213
+
214
+
215
+ ┌─────────────────┐
216
+ │ Configure │
217
+ │ Settings │
218
+ └────────┬────────┘
219
+
220
+
221
+ ┌─────────────────┐
222
+ │ PATCH /status │
223
+ │ → "published" │
224
+ └────────┬────────┘
225
+
226
+
227
+ ┌─────────────────┐
228
+ │ Students Can │
229
+ │ Submit │
230
+ └─────────────────┘
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Component Structure
236
+
237
+ ```
238
+ AssignmentModule/
239
+ ├── instructor/
240
+ │ ├── AssignmentList.tsx # List with filters/sorting
241
+ │ ├── AssignmentCreate.tsx # Create form
242
+ │ ├── AssignmentEdit.tsx # Edit settings
243
+ │ ├── InstructionUpload.tsx # Upload instruction files
244
+ │ ├── SubmissionList.tsx # View all submissions
245
+ │ └── GradingPanel.tsx # Grade with score/feedback
246
+ ├── student/
247
+ │ ├── AssignmentList.tsx # View available assignments
248
+ │ ├── AssignmentView.tsx # View details/instructions
249
+ │ ├── SubmissionForm.tsx # Submit assignment
250
+ │ ├── FileUpload.tsx # Upload file component
251
+ │ └── MySubmission.tsx # View submission & grade
252
+ └── shared/
253
+ ├── DueDateBadge.tsx # Due date with countdown
254
+ ├── LateBadge.tsx # Late submission indicator
255
+ ├── StatusBadge.tsx # Assignment status
256
+ └── FilePreviewer.tsx # Preview uploaded files
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Code Examples
262
+
263
+ ### Create Assignment
264
+
265
+ ```typescript
266
+ // POST /api/assignments
267
+ const createAssignment = async (data: CreateAssignmentDto): Promise<Assignment> => {
268
+ const response = await fetch('/api/assignments', {
269
+ method: 'POST',
270
+ headers: {
271
+ 'Content-Type': 'application/json',
272
+ 'Authorization': `Bearer ${token}`,
273
+ },
274
+ body: JSON.stringify({
275
+ courseId: 1,
276
+ title: 'Research Paper',
277
+ description: 'Write a 10-page research paper on a topic of your choice.',
278
+ instructions: '## Requirements\n- 10 pages minimum\n- APA format\n- Include references',
279
+ dueDate: '2025-03-01T23:59:59Z',
280
+ maxScore: 100,
281
+ weight: 20,
282
+ submissionType: 'file',
283
+ maxFileSize: 10485760, // 10MB
284
+ allowedFileTypes: ['pdf', 'docx'],
285
+ latePenalty: 10, // 10% per day
286
+ }),
287
+ });
288
+ return response.json();
289
+ };
290
+ ```
291
+
292
+ ### Upload Instruction File
293
+
294
+ ```typescript
295
+ // POST /api/assignments/:id/instructions/upload
296
+ const uploadInstructions = async (assignmentId: number, file: File): Promise<void> => {
297
+ const formData = new FormData();
298
+ formData.append('file', file);
299
+
300
+ await fetch(`/api/assignments/${assignmentId}/instructions/upload`, {
301
+ method: 'POST',
302
+ headers: {
303
+ 'Authorization': `Bearer ${token}`,
304
+ },
305
+ body: formData,
306
+ });
307
+ };
308
+ ```
309
+
310
+ ### Change Assignment Status
311
+
312
+ ```typescript
313
+ // PATCH /api/assignments/:id/status
314
+ const publishAssignment = async (assignmentId: number): Promise<Assignment> => {
315
+ const response = await fetch(`/api/assignments/${assignmentId}/status`, {
316
+ method: 'PATCH',
317
+ headers: {
318
+ 'Content-Type': 'application/json',
319
+ 'Authorization': `Bearer ${token}`,
320
+ },
321
+ body: JSON.stringify({ status: 'published' }),
322
+ });
323
+ return response.json();
324
+ };
325
+ ```
326
+
327
+ ### Submit Assignment (Text)
328
+
329
+ ```typescript
330
+ // POST /api/assignments/:id/submit
331
+ const submitText = async (assignmentId: number, text: string): Promise<AssignmentSubmission> => {
332
+ const response = await fetch(`/api/assignments/${assignmentId}/submit`, {
333
+ method: 'POST',
334
+ headers: {
335
+ 'Content-Type': 'application/json',
336
+ 'Authorization': `Bearer ${token}`,
337
+ },
338
+ body: JSON.stringify({ submissionText: text }),
339
+ });
340
+ return response.json();
341
+ };
342
+ ```
343
+
344
+ ### Submit Assignment (File Upload)
345
+
346
+ ```typescript
347
+ // Step 1: Upload file to Drive
348
+ // POST /api/assignments/:id/submissions/upload
349
+ const uploadFile = async (assignmentId: number, file: File): Promise<{ fileId: number }> => {
350
+ const formData = new FormData();
351
+ formData.append('file', file);
352
+
353
+ const response = await fetch(`/api/assignments/${assignmentId}/submissions/upload`, {
354
+ method: 'POST',
355
+ headers: {
356
+ 'Authorization': `Bearer ${token}`,
357
+ },
358
+ body: formData,
359
+ });
360
+ return response.json();
361
+ };
362
+
363
+ // Step 2: Submit with file ID
364
+ // POST /api/assignments/:id/submit
365
+ const submitWithFile = async (assignmentId: number, fileId: number): Promise<AssignmentSubmission> => {
366
+ const response = await fetch(`/api/assignments/${assignmentId}/submit`, {
367
+ method: 'POST',
368
+ headers: {
369
+ 'Content-Type': 'application/json',
370
+ 'Authorization': `Bearer ${token}`,
371
+ },
372
+ body: JSON.stringify({ fileId }),
373
+ });
374
+ return response.json();
375
+ };
376
+ ```
377
+
378
+ ### Submit Assignment (Link)
379
+
380
+ ```typescript
381
+ // POST /api/assignments/:id/submit
382
+ const submitLink = async (assignmentId: number, link: string): Promise<AssignmentSubmission> => {
383
+ const response = await fetch(`/api/assignments/${assignmentId}/submit`, {
384
+ method: 'POST',
385
+ headers: {
386
+ 'Content-Type': 'application/json',
387
+ 'Authorization': `Bearer ${token}`,
388
+ },
389
+ body: JSON.stringify({ submissionLink: link }),
390
+ });
391
+ return response.json();
392
+ };
393
+ ```
394
+
395
+ ### Grade Submission
396
+
397
+ ```typescript
398
+ // PATCH /api/assignments/:id/submissions/:subId/grade
399
+ const gradeSubmission = async (
400
+ assignmentId: number,
401
+ submissionId: number,
402
+ score: number,
403
+ feedback: string
404
+ ): Promise<AssignmentSubmission> => {
405
+ const response = await fetch(
406
+ `/api/assignments/${assignmentId}/submissions/${submissionId}/grade`,
407
+ {
408
+ method: 'PATCH',
409
+ headers: {
410
+ 'Content-Type': 'application/json',
411
+ 'Authorization': `Bearer ${token}`,
412
+ },
413
+ body: JSON.stringify({ score, feedback }),
414
+ }
415
+ );
416
+ return response.json();
417
+ };
418
+ ```
419
+
420
+ ### Get My Submission
421
+
422
+ ```typescript
423
+ // GET /api/assignments/:id/submissions/my
424
+ const getMySubmission = async (assignmentId: number): Promise<AssignmentSubmission | null> => {
425
+ const response = await fetch(`/api/assignments/${assignmentId}/submissions/my`, {
426
+ headers: {
427
+ 'Authorization': `Bearer ${token}`,
428
+ },
429
+ });
430
+
431
+ if (response.status === 404) {
432
+ return null; // No submission yet
433
+ }
434
+
435
+ return response.json();
436
+ };
437
+ ```
438
+
439
+ ### Due Date Badge Component
440
+
441
+ ```typescript
442
+ // components/DueDateBadge.tsx
443
+ import { useMemo } from 'react';
444
+
445
+ interface DueDateBadgeProps {
446
+ dueDate: Date;
447
+ }
448
+
449
+ export function DueDateBadge({ dueDate }: DueDateBadgeProps) {
450
+ const { status, text } = useMemo(() => {
451
+ const now = new Date();
452
+ const due = new Date(dueDate);
453
+ const diff = due.getTime() - now.getTime();
454
+ const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
455
+
456
+ if (diff < 0) {
457
+ return { status: 'overdue', text: 'Overdue' };
458
+ } else if (days === 0) {
459
+ return { status: 'today', text: 'Due Today' };
460
+ } else if (days === 1) {
461
+ return { status: 'soon', text: 'Due Tomorrow' };
462
+ } else if (days <= 3) {
463
+ return { status: 'soon', text: `Due in ${days} days` };
464
+ } else {
465
+ return { status: 'normal', text: due.toLocaleDateString() };
466
+ }
467
+ }, [dueDate]);
468
+
469
+ return (
470
+ <span className={`badge badge-${status}`}>
471
+ {text}
472
+ </span>
473
+ );
474
+ }
475
+ ```
476
+
477
+ ---
478
+
479
+ ## Error Handling
480
+
481
+ | Error | Status | Message | UI Action |
482
+ |-------|--------|---------|-----------|
483
+ | Assignment not found | 404 | Assignment not found | Show error, redirect |
484
+ | Not enrolled | 403 | Not enrolled in course | Show enrollment prompt |
485
+ | Assignment closed | 400 | Assignment is closed | Disable submit button |
486
+ | File too large | 400 | File exceeds max size | Show size limit |
487
+ | Invalid file type | 400 | File type not allowed | Show allowed types |
488
+ | Already submitted | 400 | Submission exists | Show resubmit option |
489
+
490
+ ---
491
+
492
+ ## Best Practices
493
+
494
+ 1. **Auto-save drafts** - Save text drafts to localStorage
495
+ 2. **File validation** - Check file size/type before upload
496
+ 3. **Progress indicator** - Show upload progress for large files
497
+ 4. **Confirmation** - Confirm before final submission
498
+ 5. **Late warning** - Show warning if submitting after due date
499
+ 6. **Offline queue** - Queue submissions if offline
500
+
501
+ ---
502
+
503
+ ## Related Modules
504
+
505
+ - [Grades Module](/api/grades) - Assignment scores saved to grades table
506
+ - [Google Drive Module](/api/google-drive) - File storage
507
+ - [Courses Module](/api/courses) - Assignments belong to courses
508
+ - [Notifications Module](/api/notifications) - Due date reminders
Documentation/LABS_FRONTEND_GUIDE.md ADDED
@@ -0,0 +1,661 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Labs Frontend Integration Guide
2
+
3
+ > Complete API reference and integration guide for building lab functionality in the EduVerse frontend.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Overview](#overview)
10
+ 2. [User Stories](#user-stories)
11
+ 3. [API Endpoints](#api-endpoints)
12
+ 4. [Data Models](#data-models)
13
+ 5. [Flow Diagrams](#flow-diagrams)
14
+ 6. [Component Structure](#component-structure)
15
+ 7. [Code Examples](#code-examples)
16
+
17
+ ---
18
+
19
+ ## Overview
20
+
21
+ The Labs module supports:
22
+ - **Step-by-step Instructions**: Ordered instruction cards with text and files
23
+ - **TA Materials**: Private materials (answer keys, rubrics) visible only to TAs/instructors
24
+ - **Attendance Tracking**: Mark students as present/absent/late/excused
25
+ - **Submissions**: Text or file upload via Google Drive
26
+ - **Status**: draft → published → closed → archived
27
+ - **Grading**: Manual grading with score and feedback
28
+
29
+ ---
30
+
31
+ ## User Stories
32
+
33
+ ### Instructor
34
+
35
+ | Story | Description |
36
+ |-------|-------------|
37
+ | **Create Lab** | Create lab with title, description, due date, max score |
38
+ | **Add Instructions** | Add step-by-step instructions with text and/or files |
39
+ | **Upload TA Materials** | Upload answer keys, rubrics (private to TAs) |
40
+ | **Publish** | Change status to make visible to students |
41
+ | **View Submissions** | See all student submissions |
42
+ | **Grade** | Assign score and feedback |
43
+ | **View Attendance** | See attendance report for lab session |
44
+
45
+ ### Teaching Assistant
46
+
47
+ | Story | Description |
48
+ |-------|-------------|
49
+ | **Mark Attendance** | Mark individual student attendance (present/absent/late/excused) |
50
+ | **View TA Materials** | Access answer keys and rubrics |
51
+ | **Grade Submissions** | Grade assigned student submissions |
52
+ | **View Instructions** | View all lab instructions |
53
+
54
+ ### Student
55
+
56
+ | Story | Description |
57
+ |-------|-------------|
58
+ | **View Labs** | See all labs for enrolled courses |
59
+ | **View Instructions** | See step-by-step lab instructions |
60
+ | **Submit Work** | Submit text or upload file |
61
+ | **Check Attendance** | See my attendance status |
62
+ | **View Grade** | See score and feedback after grading |
63
+
64
+ ---
65
+
66
+ ## API Endpoints
67
+
68
+ ### Lab CRUD
69
+
70
+ | Method | Endpoint | Description | Roles |
71
+ |--------|----------|-------------|-------|
72
+ | `GET` | `/api/labs` | List labs (with filters) | All authenticated |
73
+ | `GET` | `/api/labs/:id` | Get lab details | All authenticated |
74
+ | `POST` | `/api/labs` | Create lab | INSTRUCTOR, ADMIN |
75
+ | `PUT` | `/api/labs/:id` | Update lab | INSTRUCTOR, ADMIN |
76
+ | `DELETE` | `/api/labs/:id` | Delete lab | INSTRUCTOR, ADMIN |
77
+ | `PATCH` | `/api/labs/:id/status` | Change status | INSTRUCTOR, TA, ADMIN |
78
+
79
+ ### Instructions
80
+
81
+ | Method | Endpoint | Description | Roles |
82
+ |--------|----------|-------------|-------|
83
+ | `GET` | `/api/labs/:id/instructions` | Get all instructions | All authenticated |
84
+ | `POST` | `/api/labs/:id/instructions` | Add instruction | INSTRUCTOR, TA, ADMIN |
85
+ | `POST` | `/api/labs/:id/instructions/upload` | Upload instruction file | INSTRUCTOR, TA, ADMIN |
86
+
87
+ ### TA Materials
88
+
89
+ | Method | Endpoint | Description | Roles |
90
+ |--------|----------|-------------|-------|
91
+ | `POST` | `/api/labs/:id/ta-materials/upload` | Upload TA material | INSTRUCTOR, TA, ADMIN |
92
+
93
+ ### Submissions
94
+
95
+ | Method | Endpoint | Description | Roles |
96
+ |--------|----------|-------------|-------|
97
+ | `GET` | `/api/labs/:id/submissions` | List all submissions | INSTRUCTOR, TA |
98
+ | `GET` | `/api/labs/:id/submissions/my` | Get my submission | STUDENT |
99
+ | `POST` | `/api/labs/:id/submit` | Submit lab work | STUDENT |
100
+ | `POST` | `/api/labs/:id/submissions/upload` | Upload file to Drive | STUDENT |
101
+
102
+ ### Grading
103
+
104
+ | Method | Endpoint | Description | Roles |
105
+ |--------|----------|-------------|-------|
106
+ | `PATCH` | `/api/labs/:id/submissions/:subId/grade` | Grade submission | INSTRUCTOR, TA |
107
+
108
+ ### Attendance
109
+
110
+ | Method | Endpoint | Description | Roles |
111
+ |--------|----------|-------------|-------|
112
+ | `GET` | `/api/labs/:id/attendance` | Get attendance list | INSTRUCTOR, TA |
113
+ | `POST` | `/api/labs/:id/attendance` | Mark attendance | INSTRUCTOR, TA |
114
+
115
+ ---
116
+
117
+ ## Data Models
118
+
119
+ ### Lab
120
+
121
+ ```typescript
122
+ interface Lab {
123
+ labId: number;
124
+ courseId: number;
125
+ title: string;
126
+ description?: string;
127
+ dueDate?: Date;
128
+ maxScore: number;
129
+ weight?: number;
130
+ status: 'draft' | 'published' | 'closed' | 'archived';
131
+ instructions?: LabInstruction[];
132
+ createdAt: Date;
133
+ updatedAt: Date;
134
+ }
135
+ ```
136
+
137
+ ### LabInstruction
138
+
139
+ ```typescript
140
+ interface LabInstruction {
141
+ instructionId: number;
142
+ labId: number;
143
+ instructionText?: string; // Markdown supported
144
+ fileId?: number; // Drive file ID
145
+ file?: DriveFile;
146
+ orderIndex: number;
147
+ createdAt: Date;
148
+ }
149
+ ```
150
+
151
+ ### LabSubmission
152
+
153
+ ```typescript
154
+ interface LabSubmission {
155
+ submissionId: number;
156
+ labId: number;
157
+ userId: number;
158
+ user?: User;
159
+ submissionText?: string;
160
+ fileId?: number;
161
+ file?: DriveFile;
162
+ status: 'submitted' | 'graded' | 'returned' | 'resubmit';
163
+ score?: number;
164
+ feedback?: string;
165
+ gradedBy?: number;
166
+ gradedAt?: Date;
167
+ isLate: boolean;
168
+ submittedAt: Date;
169
+ }
170
+ ```
171
+
172
+ ### LabAttendance
173
+
174
+ ```typescript
175
+ interface LabAttendance {
176
+ attendanceId: number;
177
+ labId: number;
178
+ userId: number;
179
+ user?: User;
180
+ status: 'present' | 'absent' | 'excused' | 'late';
181
+ notes?: string;
182
+ markedBy: number;
183
+ markedAt: Date;
184
+ }
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Flow Diagrams
190
+
191
+ ### Lab Session Flow
192
+
193
+ ```
194
+ ┌─────────────────┐
195
+ │ Instructor │
196
+ │ Creates Lab │
197
+ └────────┬────────┘
198
+
199
+
200
+ ┌─────────────────┐
201
+ │ Add Instructions│
202
+ │ (Step by Step) │
203
+ └────────┬────────┘
204
+
205
+
206
+ ┌─────────────────┐
207
+ │ Upload TA │
208
+ │ Materials │
209
+ └────────┬────────┘
210
+
211
+
212
+ ┌─────────────────┐
213
+ │ PATCH /status │
214
+ │ → "published" │
215
+ └────────┬────────┘
216
+
217
+
218
+ ┌─────────────────────────────────────┐
219
+ │ LAB SESSION │
220
+ │ ┌──────────┐ ┌──────────────┐│
221
+ │ │ TA │ │ Students ││
222
+ │ │ │ │ ││
223
+ │ │ Mark │ │ Follow ││
224
+ │ │ Attend. │ │ Instructions ││
225
+ │ │ │ │ ││
226
+ │ │ View TA │ │ Submit Work ││
227
+ │ │ Materials│ │ ││
228
+ │ │ │ │ Check Grade ││
229
+ │ │ Grade │ │ ││
230
+ │ └──────────┘ └──────────────┘│
231
+ └─────────────────────────────────────┘
232
+ ```
233
+
234
+ ### Submission & Grading Flow
235
+
236
+ ```
237
+ ┌─────────────────┐
238
+ │ Student Views │
239
+ │ Lab Instructions│
240
+ └────────┬────────┘
241
+
242
+
243
+ ┌─────────────────┐
244
+ │ Complete Lab │
245
+ │ Work │
246
+ └────────┬────────┘
247
+
248
+
249
+ ┌─────────────────────────────┐
250
+ │ Submit: │
251
+ │ ┌───────────┐ ┌───────────┐│
252
+ │ │ Text │ │ File ││
253
+ │ │ (markdown)│ │ (upload) ││
254
+ │ └─────┬─────┘ └─────┬─────┘│
255
+ └───────┼─────────────┼──────┘
256
+ └──────┬──────┘
257
+
258
+
259
+ ┌─────────────────┐
260
+ │ POST /submit │
261
+ └────────┬────────┘
262
+
263
+
264
+ ┌─────────────────┐
265
+ │ status: │
266
+ │ "submitted" │
267
+ └────────┬────────┘
268
+
269
+ TA Grades
270
+
271
+
272
+ ┌─────────────────┐
273
+ │ PATCH /grade │
274
+ │ score + feedback│
275
+ └────────┬────────┘
276
+
277
+
278
+ ┌─────────────────┐
279
+ │ status: "graded"│
280
+ │ → grades table │
281
+ └─────────────────┘
282
+ ```
283
+
284
+ ---
285
+
286
+ ## Component Structure
287
+
288
+ ```
289
+ LabModule/
290
+ ├── instructor/
291
+ │ ├── LabList.tsx # List all labs
292
+ │ ├── LabCreate.tsx # Create lab form
293
+ │ ├── LabEdit.tsx # Edit lab settings
294
+ │ ├── InstructionEditor.tsx # Add/edit instructions
295
+ │ ├── TaMaterialUpload.tsx # Upload answer keys
296
+ │ ├── SubmissionList.tsx # View all submissions
297
+ │ └── AttendanceSheet.tsx # Full attendance grid
298
+ ├── ta/
299
+ │ ├── AttendanceMarker.tsx # Mark individual attendance
300
+ │ ├── SubmissionGrader.tsx # Grade submissions
301
+ │ ├── TaMaterials.tsx # View answer keys/rubrics
302
+ │ └── MyAssignedLabs.tsx # Labs assigned to TA
303
+ ├── student/
304
+ │ ├── LabList.tsx # View available labs
305
+ │ ├── LabView.tsx # View lab details
306
+ │ ├── InstructionViewer.tsx # Step-through instructions
307
+ │ ├── SubmissionForm.tsx # Submit work
308
+ │ └── MySubmission.tsx # View grade/feedback
309
+ └── shared/
310
+ ├── InstructionCard.tsx # Single instruction display
311
+ ├── AttendanceBadge.tsx # Present/absent/late badge
312
+ ├── LabStatusBadge.tsx # Draft/published/closed
313
+ └── StepProgress.tsx # Instruction step progress
314
+ ```
315
+
316
+ ---
317
+
318
+ ## Code Examples
319
+
320
+ ### Create Lab
321
+
322
+ ```typescript
323
+ // POST /api/labs
324
+ const createLab = async (data: CreateLabDto): Promise<Lab> => {
325
+ const response = await fetch('/api/labs', {
326
+ method: 'POST',
327
+ headers: {
328
+ 'Content-Type': 'application/json',
329
+ 'Authorization': `Bearer ${token}`,
330
+ },
331
+ body: JSON.stringify({
332
+ courseId: 1,
333
+ title: 'Lab 1: Introduction to React',
334
+ description: 'Hands-on introduction to React components and hooks.',
335
+ dueDate: '2025-02-15T17:00:00Z',
336
+ maxScore: 50,
337
+ weight: 10,
338
+ }),
339
+ });
340
+ return response.json();
341
+ };
342
+ ```
343
+
344
+ ### Add Instruction
345
+
346
+ ```typescript
347
+ // POST /api/labs/:id/instructions
348
+ const addInstruction = async (labId: number, text: string, orderIndex: number): Promise<LabInstruction> => {
349
+ const response = await fetch(`/api/labs/${labId}/instructions`, {
350
+ method: 'POST',
351
+ headers: {
352
+ 'Content-Type': 'application/json',
353
+ 'Authorization': `Bearer ${token}`,
354
+ },
355
+ body: JSON.stringify({
356
+ instructionText: text,
357
+ orderIndex,
358
+ }),
359
+ });
360
+ return response.json();
361
+ };
362
+ ```
363
+
364
+ ### Upload Instruction File
365
+
366
+ ```typescript
367
+ // POST /api/labs/:id/instructions/upload
368
+ const uploadInstructionFile = async (labId: number, file: File): Promise<LabInstruction> => {
369
+ const formData = new FormData();
370
+ formData.append('file', file);
371
+
372
+ const response = await fetch(`/api/labs/${labId}/instructions/upload`, {
373
+ method: 'POST',
374
+ headers: {
375
+ 'Authorization': `Bearer ${token}`,
376
+ },
377
+ body: formData,
378
+ });
379
+ return response.json();
380
+ };
381
+ ```
382
+
383
+ ### Upload TA Material
384
+
385
+ ```typescript
386
+ // POST /api/labs/:id/ta-materials/upload
387
+ const uploadTaMaterial = async (labId: number, file: File): Promise<void> => {
388
+ const formData = new FormData();
389
+ formData.append('file', file);
390
+
391
+ await fetch(`/api/labs/${labId}/ta-materials/upload`, {
392
+ method: 'POST',
393
+ headers: {
394
+ 'Authorization': `Bearer ${token}`,
395
+ },
396
+ body: formData,
397
+ });
398
+ };
399
+ ```
400
+
401
+ ### Change Lab Status
402
+
403
+ ```typescript
404
+ // PATCH /api/labs/:id/status
405
+ const publishLab = async (labId: number): Promise<Lab> => {
406
+ const response = await fetch(`/api/labs/${labId}/status`, {
407
+ method: 'PATCH',
408
+ headers: {
409
+ 'Content-Type': 'application/json',
410
+ 'Authorization': `Bearer ${token}`,
411
+ },
412
+ body: JSON.stringify({ status: 'published' }),
413
+ });
414
+ return response.json();
415
+ };
416
+ ```
417
+
418
+ ### Mark Attendance
419
+
420
+ ```typescript
421
+ // POST /api/labs/:id/attendance
422
+ const markAttendance = async (
423
+ labId: number,
424
+ userId: number,
425
+ status: 'present' | 'absent' | 'excused' | 'late',
426
+ notes?: string
427
+ ): Promise<LabAttendance> => {
428
+ const response = await fetch(`/api/labs/${labId}/attendance`, {
429
+ method: 'POST',
430
+ headers: {
431
+ 'Content-Type': 'application/json',
432
+ 'Authorization': `Bearer ${token}`,
433
+ },
434
+ body: JSON.stringify({ userId, status, notes }),
435
+ });
436
+ return response.json();
437
+ };
438
+ ```
439
+
440
+ ### Submit Lab Work
441
+
442
+ ```typescript
443
+ // POST /api/labs/:id/submit
444
+ const submitLab = async (labId: number, text: string): Promise<LabSubmission> => {
445
+ const response = await fetch(`/api/labs/${labId}/submit`, {
446
+ method: 'POST',
447
+ headers: {
448
+ 'Content-Type': 'application/json',
449
+ 'Authorization': `Bearer ${token}`,
450
+ },
451
+ body: JSON.stringify({ submissionText: text }),
452
+ });
453
+ return response.json();
454
+ };
455
+ ```
456
+
457
+ ### Upload Submission File
458
+
459
+ ```typescript
460
+ // POST /api/labs/:id/submissions/upload
461
+ const uploadSubmissionFile = async (labId: number, file: File): Promise<{ fileId: number }> => {
462
+ const formData = new FormData();
463
+ formData.append('file', file);
464
+
465
+ const response = await fetch(`/api/labs/${labId}/submissions/upload`, {
466
+ method: 'POST',
467
+ headers: {
468
+ 'Authorization': `Bearer ${token}`,
469
+ },
470
+ body: formData,
471
+ });
472
+ return response.json();
473
+ };
474
+ ```
475
+
476
+ ### Grade Submission
477
+
478
+ ```typescript
479
+ // PATCH /api/labs/:id/submissions/:subId/grade
480
+ const gradeSubmission = async (
481
+ labId: number,
482
+ submissionId: number,
483
+ score: number,
484
+ feedback: string
485
+ ): Promise<LabSubmission> => {
486
+ const response = await fetch(
487
+ `/api/labs/${labId}/submissions/${submissionId}/grade`,
488
+ {
489
+ method: 'PATCH',
490
+ headers: {
491
+ 'Content-Type': 'application/json',
492
+ 'Authorization': `Bearer ${token}`,
493
+ },
494
+ body: JSON.stringify({ score, feedback }),
495
+ }
496
+ );
497
+ return response.json();
498
+ };
499
+ ```
500
+
501
+ ### Instruction Viewer Component
502
+
503
+ ```typescript
504
+ // components/InstructionViewer.tsx
505
+ import { useState } from 'react';
506
+
507
+ interface InstructionViewerProps {
508
+ instructions: LabInstruction[];
509
+ }
510
+
511
+ export function InstructionViewer({ instructions }: InstructionViewerProps) {
512
+ const [currentStep, setCurrentStep] = useState(0);
513
+ const sorted = [...instructions].sort((a, b) => a.orderIndex - b.orderIndex);
514
+
515
+ const current = sorted[currentStep];
516
+ const canGoPrev = currentStep > 0;
517
+ const canGoNext = currentStep < sorted.length - 1;
518
+
519
+ return (
520
+ <div className="instruction-viewer">
521
+ {/* Progress */}
522
+ <div className="step-progress">
523
+ {sorted.map((_, idx) => (
524
+ <div
525
+ key={idx}
526
+ className={`step ${idx === currentStep ? 'active' : ''} ${idx < currentStep ? 'completed' : ''}`}
527
+ onClick={() => setCurrentStep(idx)}
528
+ />
529
+ ))}
530
+ </div>
531
+
532
+ {/* Current Instruction */}
533
+ <div className="instruction-card">
534
+ <h3>Step {currentStep + 1} of {sorted.length}</h3>
535
+
536
+ {current.instructionText && (
537
+ <div className="instruction-text"
538
+ dangerouslySetInnerHTML={{ __html: renderMarkdown(current.instructionText) }} />
539
+ )}
540
+
541
+ {current.file && (
542
+ <a href={current.file.webViewLink} target="_blank" rel="noopener noreferrer">
543
+ 📎 {current.file.fileName}
544
+ </a>
545
+ )}
546
+ </div>
547
+
548
+ {/* Navigation */}
549
+ <div className="navigation">
550
+ <button disabled={!canGoPrev} onClick={() => setCurrentStep(s => s - 1)}>
551
+ ← Previous
552
+ </button>
553
+ <button disabled={!canGoNext} onClick={() => setCurrentStep(s => s + 1)}>
554
+ Next →
555
+ </button>
556
+ </div>
557
+ </div>
558
+ );
559
+ }
560
+ ```
561
+
562
+ ### Attendance Marker Component
563
+
564
+ ```typescript
565
+ // components/AttendanceMarker.tsx
566
+ import { useState } from 'react';
567
+
568
+ interface AttendanceMarkerProps {
569
+ labId: number;
570
+ students: User[];
571
+ attendance: LabAttendance[];
572
+ onMark: (userId: number, status: string, notes?: string) => Promise<void>;
573
+ }
574
+
575
+ export function AttendanceMarker({ labId, students, attendance, onMark }: AttendanceMarkerProps) {
576
+ const getStatus = (userId: number) => {
577
+ const record = attendance.find(a => a.userId === userId);
578
+ return record?.status || 'not_marked';
579
+ };
580
+
581
+ const statusOptions = ['present', 'absent', 'late', 'excused'];
582
+
583
+ return (
584
+ <div className="attendance-marker">
585
+ <table>
586
+ <thead>
587
+ <tr>
588
+ <th>Student</th>
589
+ <th>Status</th>
590
+ <th>Notes</th>
591
+ </tr>
592
+ </thead>
593
+ <tbody>
594
+ {students.map(student => (
595
+ <tr key={student.userId}>
596
+ <td>{student.firstName} {student.lastName}</td>
597
+ <td>
598
+ <select
599
+ value={getStatus(student.userId)}
600
+ onChange={(e) => onMark(student.userId, e.target.value)}
601
+ >
602
+ <option value="not_marked">-- Select --</option>
603
+ {statusOptions.map(status => (
604
+ <option key={status} value={status}>
605
+ {status.charAt(0).toUpperCase() + status.slice(1)}
606
+ </option>
607
+ ))}
608
+ </select>
609
+ </td>
610
+ <td>
611
+ <input
612
+ type="text"
613
+ placeholder="Optional notes"
614
+ onBlur={(e) => {
615
+ if (e.target.value) {
616
+ onMark(student.userId, getStatus(student.userId), e.target.value);
617
+ }
618
+ }}
619
+ />
620
+ </td>
621
+ </tr>
622
+ ))}
623
+ </tbody>
624
+ </table>
625
+ </div>
626
+ );
627
+ }
628
+ ```
629
+
630
+ ---
631
+
632
+ ## Error Handling
633
+
634
+ | Error | Status | Message | UI Action |
635
+ |-------|--------|---------|-----------|
636
+ | Lab not found | 404 | Lab not found | Show error, redirect |
637
+ | Not enrolled | 403 | Not enrolled in course | Show enrollment prompt |
638
+ | Lab closed | 400 | Lab is closed | Disable submit |
639
+ | Already submitted | 400 | Submission exists | Show resubmit option |
640
+ | File too large | 400 | File exceeds max size | Show size limit |
641
+ | Attendance exists | 400 | Attendance already marked | Show update option |
642
+
643
+ ---
644
+
645
+ ## Best Practices
646
+
647
+ 1. **Step progress persistence** - Save current step to localStorage
648
+ 2. **Auto-save drafts** - Save submission drafts
649
+ 3. **Attendance confirmation** - Confirm before marking absent
650
+ 4. **File preview** - Preview files before submission
651
+ 5. **Offline support** - Queue attendance marks if offline
652
+ 6. **Real-time updates** - Use WebSocket for live attendance status
653
+
654
+ ---
655
+
656
+ ## Related Modules
657
+
658
+ - [Grades Module](/api/grades) - Lab scores saved to grades table
659
+ - [Google Drive Module](/api/google-drive) - File storage
660
+ - [Courses Module](/api/courses) - Labs belong to courses
661
+ - [Users Module](/api/users) - Student/TA info for attendance
Documentation/QUIZZES_FRONTEND_GUIDE.md ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Quizzes Frontend Integration Guide
2
+
3
+ > Complete API reference and integration guide for building quiz functionality in the EduVerse frontend.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Overview](#overview)
10
+ 2. [User Stories](#user-stories)
11
+ 3. [API Endpoints](#api-endpoints)
12
+ 4. [Data Models](#data-models)
13
+ 5. [Flow Diagrams](#flow-diagrams)
14
+ 6. [Component Structure](#component-structure)
15
+ 7. [Code Examples](#code-examples)
16
+
17
+ ---
18
+
19
+ ## Overview
20
+
21
+ The Quizzes module supports:
22
+ - **Question Types**: MCQ, True/False, Short Answer, Essay, Matching
23
+ - **Auto-grading**: MCQ, True/False, Short Answer (exact match)
24
+ - **Manual grading**: Essay, Short Answer (flexible grading)
25
+ - **Settings**: Time limits, max attempts, randomization, passing score
26
+ - **Status**: draft → published → closed → archived
27
+
28
+ ---
29
+
30
+ ## User Stories
31
+
32
+ ### Instructor
33
+
34
+ | Story | Description |
35
+ |-------|-------------|
36
+ | **Create Quiz** | Create a quiz with title, description, settings (time limit, max attempts, passing score) |
37
+ | **Add Questions** | Add questions with types: MCQ, true/false, short answer, essay, matching |
38
+ | **Reorder Questions** | Drag-and-drop to reorder questions |
39
+ | **Publish/Unpublish** | Change quiz status (draft/published/closed/archived) |
40
+ | **View Attempts** | See all student attempts with scores |
41
+ | **Manual Grade** | Grade essay/short answer questions with points and feedback |
42
+ | **View Statistics** | See quiz analytics (avg score, pass rate, question analysis) |
43
+
44
+ ### Student
45
+
46
+ | Story | Description |
47
+ |-------|-------------|
48
+ | **View Quizzes** | See available quizzes for enrolled courses |
49
+ | **Start Quiz** | Begin a new attempt (if attempts remaining) |
50
+ | **Answer Questions** | Answer questions with timer countdown |
51
+ | **Submit Quiz** | Submit answers and see immediate results (auto-graded) |
52
+ | **View Results** | See detailed results with correct answers (after submission) |
53
+ | **View History** | See all past attempts with scores |
54
+
55
+ ---
56
+
57
+ ## API Endpoints
58
+
59
+ ### Quiz CRUD
60
+
61
+ | Method | Endpoint | Description | Roles |
62
+ |--------|----------|-------------|-------|
63
+ | `GET` | `/api/quizzes` | List quizzes (with filters) | All authenticated |
64
+ | `GET` | `/api/quizzes/:id` | Get quiz details | All authenticated |
65
+ | `POST` | `/api/quizzes` | Create quiz | INSTRUCTOR, TA, ADMIN |
66
+ | `PUT` | `/api/quizzes/:id` | Update quiz | INSTRUCTOR, TA, ADMIN |
67
+ | `DELETE` | `/api/quizzes/:id` | Delete quiz | INSTRUCTOR, TA, ADMIN |
68
+ | `PATCH` | `/api/quizzes/:id/status` | Change status | INSTRUCTOR, TA, ADMIN |
69
+
70
+ ### Questions
71
+
72
+ | Method | Endpoint | Description | Roles |
73
+ |--------|----------|-------------|-------|
74
+ | `POST` | `/api/quizzes/:quizId/questions` | Add question | INSTRUCTOR, TA, ADMIN |
75
+ | `PUT` | `/api/quizzes/:quizId/questions/:questionId` | Update question | INSTRUCTOR, TA, ADMIN |
76
+ | `DELETE` | `/api/quizzes/:quizId/questions/:questionId` | Delete question | INSTRUCTOR, TA, ADMIN |
77
+ | `PUT` | `/api/quizzes/:quizId/questions/reorder` | Reorder questions | INSTRUCTOR, TA, ADMIN |
78
+
79
+ ### Student Attempts
80
+
81
+ | Method | Endpoint | Description | Roles |
82
+ |--------|----------|-------------|-------|
83
+ | `POST` | `/api/quizzes/:quizId/attempts/start` | Start new attempt | STUDENT |
84
+ | `POST` | `/api/quizzes/attempts/:attemptId/submit` | Submit answers | STUDENT |
85
+ | `GET` | `/api/quizzes/attempts/:attemptId` | Get attempt details | STUDENT (own), INSTRUCTOR |
86
+ | `GET` | `/api/quizzes/my-attempts` | List my attempts | STUDENT |
87
+
88
+ ### Grading
89
+
90
+ | Method | Endpoint | Description | Roles |
91
+ |--------|----------|-------------|-------|
92
+ | `GET` | `/api/quizzes/attempts/:attemptId/pending-grading` | Get pending questions | INSTRUCTOR, TA |
93
+ | `POST` | `/api/quizzes/attempts/:attemptId/grade` | Apply manual grades | INSTRUCTOR, TA |
94
+
95
+ ### Statistics
96
+
97
+ | Method | Endpoint | Description | Roles |
98
+ |--------|----------|-------------|-------|
99
+ | `GET` | `/api/quizzes/:quizId/statistics` | Quiz analytics | INSTRUCTOR, TA, ADMIN |
100
+ | `GET` | `/api/quizzes/:quizId/progress/:studentId` | Student progress | INSTRUCTOR, TA |
101
+
102
+ ---
103
+
104
+ ## Data Models
105
+
106
+ ### Quiz
107
+
108
+ ```typescript
109
+ interface Quiz {
110
+ quizId: number;
111
+ courseId: number;
112
+ title: string;
113
+ description?: string;
114
+ status: 'draft' | 'published' | 'closed' | 'archived';
115
+ timeLimitMinutes?: number;
116
+ maxAttempts?: number;
117
+ passingScore?: number;
118
+ randomizeQuestions: boolean;
119
+ randomizeOptions: boolean;
120
+ showCorrectAnswers: boolean;
121
+ availableFrom?: Date;
122
+ availableUntil?: Date;
123
+ questions?: QuizQuestion[];
124
+ createdAt: Date;
125
+ updatedAt: Date;
126
+ }
127
+ ```
128
+
129
+ ### QuizQuestion
130
+
131
+ ```typescript
132
+ interface QuizQuestion {
133
+ questionId: number;
134
+ quizId: number;
135
+ questionType: 'mcq' | 'true_false' | 'short_answer' | 'essay' | 'matching';
136
+ questionText: string;
137
+ options?: string[]; // For MCQ: ["Option A", "Option B", ...]
138
+ correctAnswer?: string; // For MCQ: "A" | For true/false: "true"/"false"
139
+ matchingPairs?: { left: string; right: string }[];
140
+ points: number;
141
+ orderIndex: number;
142
+ difficultyLevel?: QuizDifficultyLevel;
143
+ }
144
+ ```
145
+
146
+ ### QuizAttempt
147
+
148
+ ```typescript
149
+ interface QuizAttempt {
150
+ attemptId: number;
151
+ quizId: number;
152
+ userId: number;
153
+ status: 'in_progress' | 'submitted' | 'graded' | 'abandoned';
154
+ startTime: Date;
155
+ submitTime?: Date;
156
+ score?: number;
157
+ maxScore?: number;
158
+ percentage?: number;
159
+ passed?: boolean;
160
+ answers?: QuizAnswer[];
161
+ }
162
+ ```
163
+
164
+ ### QuizAnswer
165
+
166
+ ```typescript
167
+ interface QuizAnswer {
168
+ answerId: number;
169
+ attemptId: number;
170
+ questionId: number;
171
+ answerText?: string;
172
+ selectedOptions?: string[];
173
+ matchingAnswers?: { left: string; right: string }[];
174
+ isCorrect?: boolean;
175
+ pointsEarned?: number;
176
+ feedback?: string;
177
+ }
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Flow Diagrams
183
+
184
+ ### Quiz Taking Flow
185
+
186
+ ```
187
+ ┌─────────────────┐
188
+ │ Student Views │
189
+ │ Available Quiz │
190
+ └────────┬────────┘
191
+
192
+
193
+ ┌─────────────────┐
194
+ │ POST /attempts │
195
+ │ /start │
196
+ └────────┬────────┘
197
+
198
+
199
+ ┌─────────────────┐
200
+ │ Quiz In Progress│
201
+ │ Timer Running │◄──────┐
202
+ └────────┬────────┘ │
203
+ │ │
204
+ Navigate Save Progress
205
+ Questions │
206
+ │ │
207
+ ▼ │
208
+ ┌─────────────────┐ │
209
+ │ Answer Question │───────┘
210
+ └────────┬────────┘
211
+
212
+ Time Up / Submit
213
+
214
+
215
+ ┌─────────────────┐
216
+ │ POST /attempts │
217
+ │ /submit │
218
+ └────────┬────────┘
219
+
220
+
221
+ ┌─────────────────┐
222
+ │ Auto-Grade MCQ, │
223
+ │ True/False │
224
+ └────────┬────────┘
225
+
226
+
227
+ ┌─────────────────┐
228
+ │ Show Results │
229
+ │ (partial if │
230
+ │ essays pending) │
231
+ └─────────────────┘
232
+ ```
233
+
234
+ ### Quiz Creation Flow
235
+
236
+ ```
237
+ ┌─────────────────┐
238
+ │ Instructor │
239
+ │ Creates Quiz │
240
+ └────────┬────────┘
241
+
242
+
243
+ ┌─────────────────┐
244
+ │ POST /quizzes │
245
+ │ (status: draft) │
246
+ └────────┬────────┘
247
+
248
+
249
+ ┌─────────────────┐
250
+ │ Add Questions │
251
+ │ POST /questions │
252
+ └────────┬────────┘
253
+
254
+
255
+ ┌─────────────────┐
256
+ │ Reorder/Edit │
257
+ │ Questions │
258
+ └────────┬────────┘
259
+
260
+
261
+ ┌─────────────────┐
262
+ │ PATCH /status │
263
+ │ → "published" │
264
+ └────────┬────────┘
265
+
266
+
267
+ ┌─────────────────┐
268
+ │ Students Can │
269
+ │ Take Quiz │
270
+ └─────────────────┘
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Component Structure
276
+
277
+ ```
278
+ QuizModule/
279
+ ├── instructor/
280
+ │ ├── QuizList.tsx # List all quizzes with filters
281
+ │ ├── QuizCreate.tsx # Create new quiz form
282
+ │ ├── QuizEdit.tsx # Edit quiz settings
283
+ │ ├── QuestionEditor.tsx # Add/edit questions
284
+ │ ├── QuestionReorder.tsx # Drag-drop reorder
285
+ │ ├── AttemptsList.tsx # View student attempts
286
+ │ ├── GradingQueue.tsx # Manual grading UI
287
+ │ └── QuizStatistics.tsx # Analytics dashboard
288
+ ├── student/
289
+ │ ├── AvailableQuizzes.tsx # View available quizzes
290
+ │ ├── QuizTaker.tsx # Take quiz with timer
291
+ │ ├── QuestionDisplay.tsx # Render question by type
292
+ │ ├── QuizResults.tsx # View results after submit
293
+ │ └── AttemptHistory.tsx # Past attempts
294
+ └── shared/
295
+ ├── Timer.tsx # Countdown timer
296
+ ├── ProgressBar.tsx # Question progress
297
+ └── QuestionTypeIcon.tsx # Icon by question type
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Code Examples
303
+
304
+ ### Create Quiz
305
+
306
+ ```typescript
307
+ // POST /api/quizzes
308
+ const createQuiz = async (data: CreateQuizDto): Promise<Quiz> => {
309
+ const response = await fetch('/api/quizzes', {
310
+ method: 'POST',
311
+ headers: {
312
+ 'Content-Type': 'application/json',
313
+ 'Authorization': `Bearer ${token}`,
314
+ },
315
+ body: JSON.stringify({
316
+ courseId: 1,
317
+ title: 'Midterm Quiz',
318
+ description: 'Covers chapters 1-5',
319
+ timeLimitMinutes: 60,
320
+ maxAttempts: 2,
321
+ passingScore: 70,
322
+ randomizeQuestions: true,
323
+ randomizeOptions: true,
324
+ showCorrectAnswers: true,
325
+ availableFrom: '2025-02-01T08:00:00Z',
326
+ availableUntil: '2025-02-15T23:59:59Z',
327
+ }),
328
+ });
329
+ return response.json();
330
+ };
331
+ ```
332
+
333
+ ### Add MCQ Question
334
+
335
+ ```typescript
336
+ // POST /api/quizzes/:quizId/questions
337
+ const addQuestion = async (quizId: number): Promise<QuizQuestion> => {
338
+ const response = await fetch(`/api/quizzes/${quizId}/questions`, {
339
+ method: 'POST',
340
+ headers: {
341
+ 'Content-Type': 'application/json',
342
+ 'Authorization': `Bearer ${token}`,
343
+ },
344
+ body: JSON.stringify({
345
+ questionType: 'mcq',
346
+ questionText: 'What is the capital of France?',
347
+ options: ['London', 'Paris', 'Berlin', 'Madrid'],
348
+ correctAnswer: 'B',
349
+ points: 10,
350
+ }),
351
+ });
352
+ return response.json();
353
+ };
354
+ ```
355
+
356
+ ### Change Quiz Status
357
+
358
+ ```typescript
359
+ // PATCH /api/quizzes/:id/status
360
+ const publishQuiz = async (quizId: number): Promise<Quiz> => {
361
+ const response = await fetch(`/api/quizzes/${quizId}/status`, {
362
+ method: 'PATCH',
363
+ headers: {
364
+ 'Content-Type': 'application/json',
365
+ 'Authorization': `Bearer ${token}`,
366
+ },
367
+ body: JSON.stringify({ status: 'published' }),
368
+ });
369
+ return response.json();
370
+ };
371
+ ```
372
+
373
+ ### Start Quiz Attempt
374
+
375
+ ```typescript
376
+ // POST /api/quizzes/:quizId/attempts/start
377
+ const startAttempt = async (quizId: number): Promise<QuizAttempt> => {
378
+ const response = await fetch(`/api/quizzes/${quizId}/attempts/start`, {
379
+ method: 'POST',
380
+ headers: {
381
+ 'Authorization': `Bearer ${token}`,
382
+ },
383
+ });
384
+ return response.json();
385
+ };
386
+ ```
387
+
388
+ ### Submit Quiz
389
+
390
+ ```typescript
391
+ // POST /api/quizzes/attempts/:attemptId/submit
392
+ const submitQuiz = async (attemptId: number, answers: SubmitAnswerDto[]): Promise<QuizAttempt> => {
393
+ const response = await fetch(`/api/quizzes/attempts/${attemptId}/submit`, {
394
+ method: 'POST',
395
+ headers: {
396
+ 'Content-Type': 'application/json',
397
+ 'Authorization': `Bearer ${token}`,
398
+ },
399
+ body: JSON.stringify({ answers }),
400
+ });
401
+ return response.json();
402
+ };
403
+
404
+ // Example answers array
405
+ const answers: SubmitAnswerDto[] = [
406
+ { questionId: 1, answerText: 'B' }, // MCQ
407
+ { questionId: 2, answerText: 'true' }, // True/False
408
+ { questionId: 3, answerText: 'Photosynthesis' }, // Short answer
409
+ { questionId: 4, answerText: 'Essay response...' }, // Essay
410
+ ];
411
+ ```
412
+
413
+ ### Manual Grading
414
+
415
+ ```typescript
416
+ // POST /api/quizzes/attempts/:attemptId/grade
417
+ const gradeEssay = async (attemptId: number, grades: ManualGradeDto[]): Promise<void> => {
418
+ await fetch(`/api/quizzes/attempts/${attemptId}/grade`, {
419
+ method: 'POST',
420
+ headers: {
421
+ 'Content-Type': 'application/json',
422
+ 'Authorization': `Bearer ${token}`,
423
+ },
424
+ body: JSON.stringify({
425
+ grades: [
426
+ {
427
+ questionId: 4,
428
+ pointsEarned: 8,
429
+ feedback: 'Good analysis but missing conclusion.',
430
+ },
431
+ ],
432
+ }),
433
+ });
434
+ };
435
+ ```
436
+
437
+ ### Timer Component
438
+
439
+ ```typescript
440
+ // components/Timer.tsx
441
+ import { useEffect, useState } from 'react';
442
+
443
+ interface TimerProps {
444
+ endTime: Date;
445
+ onTimeUp: () => void;
446
+ }
447
+
448
+ export function Timer({ endTime, onTimeUp }: TimerProps) {
449
+ const [remaining, setRemaining] = useState<number>(0);
450
+
451
+ useEffect(() => {
452
+ const interval = setInterval(() => {
453
+ const now = new Date().getTime();
454
+ const end = new Date(endTime).getTime();
455
+ const diff = Math.max(0, Math.floor((end - now) / 1000));
456
+
457
+ setRemaining(diff);
458
+
459
+ if (diff === 0) {
460
+ clearInterval(interval);
461
+ onTimeUp();
462
+ }
463
+ }, 1000);
464
+
465
+ return () => clearInterval(interval);
466
+ }, [endTime, onTimeUp]);
467
+
468
+ const minutes = Math.floor(remaining / 60);
469
+ const seconds = remaining % 60;
470
+
471
+ return (
472
+ <div className={`timer ${remaining < 300 ? 'warning' : ''}`}>
473
+ {String(minutes).padStart(2, '0')}:{String(seconds).padStart(2, '0')}
474
+ </div>
475
+ );
476
+ }
477
+ ```
478
+
479
+ ---
480
+
481
+ ## Error Handling
482
+
483
+ | Error | Status | Message | UI Action |
484
+ |-------|--------|---------|-----------|
485
+ | Quiz not found | 404 | Quiz not found | Show error, redirect to list |
486
+ | Max attempts exceeded | 400 | Maximum attempts exceeded | Show message, disable start |
487
+ | Quiz closed | 400 | Quiz is closed | Show message |
488
+ | Time expired | 400 | Quiz time limit exceeded | Auto-submit, show results |
489
+ | Not enrolled | 403 | Not enrolled in course | Show enrollment prompt |
490
+
491
+ ---
492
+
493
+ ## Best Practices
494
+
495
+ 1. **Auto-save answers** - Save answers to localStorage during quiz to prevent data loss
496
+ 2. **Confirm submit** - Show confirmation dialog before final submit
497
+ 3. **Handle timer edge cases** - Auto-submit when timer reaches 0
498
+ 4. **Offline handling** - Queue answer submissions and retry on reconnect
499
+ 5. **Accessibility** - Ensure all question types are keyboard navigable
500
+ 6. **Progress indication** - Show which questions are answered vs unanswered
501
+
502
+ ---
503
+
504
+ ## Related Modules
505
+
506
+ - [Grades Module](/api/grades) - Quiz scores are saved to grades table
507
+ - [Courses Module](/api/courses) - Quizzes belong to courses
508
+ - [Notifications Module](/api/notifications) - Quiz deadline reminders
EduVerse API - Complete Test Collection Copy.postman_test_run1 DELETED
The diff for this file is too large to render. See raw diff
 
EduVerse API - Complete Test Collection.postman_test_run.json DELETED
The diff for this file is too large to render. See raw diff
 
EduVerse_Postman_Collection.json CHANGED
@@ -2854,6 +2854,38 @@
2854
  }
2855
  }
2856
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2857
  {
2858
  "name": "Get Difficulty Levels",
2859
  "request": {
@@ -3427,6 +3459,45 @@
3427
  }
3428
  }
3429
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3430
  {
3431
  "name": "Get Instructions",
3432
  "request": {
 
2854
  }
2855
  }
2856
  },
2857
+ {
2858
+ "name": "Change Quiz Status (Instructor)",
2859
+ "request": {
2860
+ "description": "Change quiz status: draft, published, closed, archived",
2861
+ "method": "PATCH",
2862
+ "header": [
2863
+ {
2864
+ "key": "Authorization",
2865
+ "value": "Bearer {{instructor_token}}"
2866
+ },
2867
+ {
2868
+ "key": "Content-Type",
2869
+ "value": "application/json"
2870
+ }
2871
+ ],
2872
+ "body": {
2873
+ "mode": "raw",
2874
+ "raw": "{\"status\":\"published\"}"
2875
+ },
2876
+ "url": {
2877
+ "raw": "{{baseUrl}}/quizzes/1/status",
2878
+ "host": [
2879
+ "{{baseUrl}}"
2880
+ ],
2881
+ "path": [
2882
+ "quizzes",
2883
+ "1",
2884
+ "status"
2885
+ ]
2886
+ }
2887
+ }
2888
+ },
2889
  {
2890
  "name": "Get Difficulty Levels",
2891
  "request": {
 
3459
  }
3460
  }
3461
  },
3462
+ {
3463
+ "name": "Change Lab Status",
3464
+ "request": {
3465
+ "description": "Change lab status: draft, published, closed, archived. Roles: Instructor, TA, Admin",
3466
+ "auth": {
3467
+ "bearer": [
3468
+ {
3469
+ "key": "token",
3470
+ "type": "string",
3471
+ "value": "{{instructor_token}}"
3472
+ }
3473
+ ],
3474
+ "type": "bearer"
3475
+ },
3476
+ "method": "PATCH",
3477
+ "header": [
3478
+ {
3479
+ "key": "Content-Type",
3480
+ "value": "application/json"
3481
+ }
3482
+ ],
3483
+ "body": {
3484
+ "mode": "raw",
3485
+ "raw": "{\"status\":\"published\"}"
3486
+ },
3487
+ "url": {
3488
+ "path": [
3489
+ "api",
3490
+ "labs",
3491
+ "1",
3492
+ "status"
3493
+ ],
3494
+ "raw": "{{baseUrl}}/api/labs/1/status",
3495
+ "host": [
3496
+ "{{baseUrl}}"
3497
+ ]
3498
+ }
3499
+ }
3500
+ },
3501
  {
3502
  "name": "Get Instructions",
3503
  "request": {
howtotest.md ADDED
@@ -0,0 +1,910 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # EduVerse Frontend Integration Master Plan — Test-First Edition
2
+
3
+ **Last Updated**: 2026-03-15
4
+ **Stack**: React (Vite) + NestJS Backend | **Base URL**: `http://localhost:8081`
5
+
6
+ ---
7
+
8
+ ## ✅ COMPLETED
9
+
10
+ - Phase 0 — Infrastructure
11
+ - Phase 1 — Auth + Layout + All 4 dashboard shells
12
+ - Phase 8 — WebSocket Messaging
13
+ - Course Management — Create/Edit/Delete course, multi-step modal (section + schedule + staff), instructor/TA assignment + display
14
+
15
+ ---
16
+
17
+ ## METHODOLOGY — TEST BEFORE INTEGRATING
18
+
19
+ Every task follows this exact workflow before writing a single line of frontend code:
20
+
21
+ ### Step 1 — Test the endpoint
22
+
23
+ Paste into browser console (replace TOKEN):
24
+
25
+ ```javascript
26
+ fetch('http://localhost:8081/api/ENDPOINT', {
27
+ headers: { Authorization: 'Bearer TOKEN' },
28
+ })
29
+ .then((r) => r.json())
30
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
31
+ ```
32
+
33
+ For POST endpoints:
34
+
35
+ ```javascript
36
+ fetch('http://localhost:8081/api/ENDPOINT', {
37
+ method: 'POST',
38
+ headers: {
39
+ Authorization: 'Bearer TOKEN',
40
+ 'Content-Type': 'application/json',
41
+ },
42
+ body: JSON.stringify({ ...payload }),
43
+ })
44
+ .then((r) => r.json())
45
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
46
+ ```
47
+
48
+ ### Step 2 — Check the DB if needed
49
+
50
+ ```sql
51
+ SELECT * FROM table_name LIMIT 5;
52
+ DESCRIBE table_name;
53
+ ```
54
+
55
+ ### Step 3 — Verify shape + decide
56
+
57
+ - Does it return the right fields?
58
+ - Is pagination wrapped in `{ data: [], total, page }` or flat array?
59
+ - Are IDs `id` or `courseId` or `userId`?
60
+ - Does it need a backend fix before frontend work starts?
61
+
62
+ ### Step 4 — Write the Copilot prompt with real shapes
63
+
64
+ Only after steps 1-3, write the integration prompt with actual field names.
65
+
66
+ ---
67
+
68
+ ## NEXT UP — Phase 3: Student Enrollment Flow
69
+
70
+ ### TASK: COURSE-03 — My Courses Sidebar
71
+
72
+ **Test first:**
73
+
74
+ ```javascript
75
+ // Test 1: get my enrolled courses (log in as student first)
76
+ fetch('http://localhost:8081/api/enrollments/my-courses', {
77
+ headers: { Authorization: 'Bearer STUDENT_TOKEN' },
78
+ })
79
+ .then((r) => r.json())
80
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
81
+ ```
82
+
83
+ ```sql
84
+ -- Check enrollment table structure
85
+ SELECT * FROM course_enrollments LIMIT 5;
86
+ SELECT e.*, c.name, c.code FROM course_enrollments e
87
+ JOIN courses c ON e.course_id = c.id
88
+ WHERE e.user_id = 57 LIMIT 5;
89
+ ```
90
+
91
+ **After testing** → Build `<MyCoursesSidebar />` with real field names.
92
+
93
+ ---
94
+
95
+ ### TASK: COURSE-04 — Available Courses
96
+
97
+ **Test first:**
98
+
99
+ ```javascript
100
+ fetch('http://localhost:8081/api/enrollments/available', {
101
+ headers: { Authorization: 'Bearer STUDENT_TOKEN' },
102
+ })
103
+ .then((r) => r.json())
104
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
105
+ ```
106
+
107
+ ```sql
108
+ SELECT * FROM course_sections WHERE status = 'open' LIMIT 5;
109
+ ```
110
+
111
+ **After testing** → Build `<AvailableCoursesPage />`.
112
+
113
+ ---
114
+
115
+ ### TASK: COURSE-05 — Enroll in Course
116
+
117
+ **Test first:**
118
+
119
+ ```javascript
120
+ // Test enrollment
121
+ fetch('http://localhost:8081/api/enrollments/register', {
122
+ method: 'POST',
123
+ headers: {
124
+ Authorization: 'Bearer STUDENT_TOKEN',
125
+ 'Content-Type': 'application/json',
126
+ },
127
+ body: JSON.stringify({ sectionId: 1 }),
128
+ })
129
+ .then((r) => r.json())
130
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
131
+ ```
132
+
133
+ ```sql
134
+ -- Check prerequisites table
135
+ SELECT * FROM course_prerequisites LIMIT 5;
136
+ -- Check if student meets prerequisites
137
+ SELECT * FROM course_enrollments WHERE user_id = 57;
138
+ ```
139
+
140
+ **After testing** → Build `<EnrollButton />` with section picker modal.
141
+
142
+ ---
143
+
144
+ ### TASK: COURSE-06 — Drop Course
145
+
146
+ **Test first:**
147
+
148
+ ```javascript
149
+ // Find enrollment ID first
150
+ fetch('http://localhost:8081/api/enrollments/my-courses', {
151
+ headers: { Authorization: 'Bearer STUDENT_TOKEN' },
152
+ })
153
+ .then((r) => r.json())
154
+ .then((d) =>
155
+ console.log(
156
+ 'enrollment IDs:',
157
+ d.map((e) => e.enrollmentId || e.id),
158
+ ),
159
+ );
160
+
161
+ // Then test drop
162
+ fetch('http://localhost:8081/api/enrollments/ENROLLMENT_ID', {
163
+ method: 'DELETE',
164
+ headers: { Authorization: 'Bearer STUDENT_TOKEN' },
165
+ })
166
+ .then((r) => r.json())
167
+ .then(console.log);
168
+ ```
169
+
170
+ **After testing** → Build `<DropCourseButton />`.
171
+
172
+ ---
173
+
174
+ ### TASK: COURSE-07 — Student Roster (Instructor view)
175
+
176
+ **Test first:**
177
+
178
+ ```javascript
179
+ fetch('http://localhost:8081/api/enrollments/section/1/students', {
180
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
181
+ })
182
+ .then((r) => r.json())
183
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
184
+ ```
185
+
186
+ ```sql
187
+ SELECT e.*, u.first_name, u.last_name, u.email
188
+ FROM course_enrollments e
189
+ JOIN users u ON e.user_id = u.user_id
190
+ WHERE e.section_id = 1;
191
+ ```
192
+
193
+ **After testing** → Build `<StudentRosterTable />`.
194
+
195
+ ---
196
+
197
+ ### TASK: COURSE-08 — Waitlist
198
+
199
+ **Test first:**
200
+
201
+ ```javascript
202
+ fetch('http://localhost:8081/api/enrollments/section/1/waitlist', {
203
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
204
+ })
205
+ .then((r) => r.json())
206
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
207
+ ```
208
+
209
+ **After testing** → Build `<WaitlistTable />`.
210
+
211
+ ---
212
+
213
+ ### TASK: COURSE-09 — Section Schedule View
214
+
215
+ **Test first:**
216
+
217
+ ```javascript
218
+ fetch('http://localhost:8081/api/schedules/section/1', {
219
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
220
+ })
221
+ .then((r) => r.json())
222
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
223
+ ```
224
+
225
+ **After testing** → Build `<SectionScheduleView />`.
226
+
227
+ ---
228
+
229
+ ## Phase 4 — Course Materials + Structure
230
+
231
+ ### TASK: MAT-01 — Course Structure
232
+
233
+ **Test first:**
234
+
235
+ ```javascript
236
+ fetch('http://localhost:8081/api/courses/1/structure', {
237
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
238
+ })
239
+ .then((r) => r.json())
240
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
241
+ ```
242
+
243
+ ```sql
244
+ SELECT * FROM lecture_section_labs WHERE course_id = 1 LIMIT 5;
245
+ ```
246
+
247
+ ---
248
+
249
+ ### TASK: MAT-02 — Materials List
250
+
251
+ **Test first:**
252
+
253
+ ```javascript
254
+ // As admin
255
+ fetch('http://localhost:8081/api/courses/1/materials', {
256
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
257
+ })
258
+ .then((r) => r.json())
259
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
260
+
261
+ // As student (should only see published)
262
+ fetch('http://localhost:8081/api/courses/1/materials', {
263
+ headers: { Authorization: 'Bearer STUDENT_TOKEN' },
264
+ })
265
+ .then((r) => r.json())
266
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
267
+ ```
268
+
269
+ ```sql
270
+ SELECT * FROM course_materials WHERE course_id = 1 LIMIT 5;
271
+ ```
272
+
273
+ ---
274
+
275
+ ### TASK: MAT-03 — Video Player (YouTube embed)
276
+
277
+ **Test first:**
278
+
279
+ ```javascript
280
+ // Find a material with materialType = 'video'
281
+ fetch('http://localhost:8081/api/courses/1/materials', {
282
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
283
+ })
284
+ .then((r) => r.json())
285
+ .then((d) =>
286
+ console.log(
287
+ 'video materials:',
288
+ d.filter((m) => m.materialType === 'video'),
289
+ ),
290
+ );
291
+
292
+ // Get embed URL for a video material
293
+ fetch('http://localhost:8081/api/courses/1/materials/23/embed', {
294
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
295
+ })
296
+ .then((r) => r.json())
297
+ .then(console.log);
298
+ ```
299
+
300
+ ---
301
+
302
+ ### TASK: MAT-04 — File Download
303
+
304
+ **Test first:**
305
+
306
+ ```javascript
307
+ // Find a material with a fileId
308
+ fetch('http://localhost:8081/api/courses/2/materials/4/download', {
309
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
310
+ }).then((r) =>
311
+ console.log('status:', r.status, 'type:', r.headers.get('content-type')),
312
+ );
313
+ ```
314
+
315
+ ---
316
+
317
+ ### TASK: MAT-05 — File Upload
318
+
319
+ **Test first (check multer field name):**
320
+
321
+ ```javascript
322
+ const formData = new FormData();
323
+ // Create a test file
324
+ const blob = new Blob(['test content'], { type: 'text/plain' });
325
+ formData.append('file', blob, 'test.txt');
326
+ formData.append('title', 'Test Upload');
327
+ formData.append('materialType', 'document');
328
+ formData.append('weekNumber', '1');
329
+
330
+ fetch('http://localhost:8081/api/courses/1/materials', {
331
+ method: 'POST',
332
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
333
+ body: formData,
334
+ })
335
+ .then((r) => r.json())
336
+ .then(console.log);
337
+ ```
338
+
339
+ ---
340
+
341
+ ### TASK: MAT-06 — Video Upload (YouTube)
342
+
343
+ **Test first:**
344
+
345
+ ```javascript
346
+ // Check if YouTube is configured
347
+ fetch('http://localhost:8081/youtube/auth', {
348
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
349
+ })
350
+ .then((r) => r.json())
351
+ .then(console.log);
352
+ ```
353
+
354
+ ```sql
355
+ -- Check if YouTube tokens are stored
356
+ SELECT * FROM youtube_tokens LIMIT 1;
357
+ -- or check env
358
+ ```
359
+
360
+ ---
361
+
362
+ ## Phase 5 — Assignments + Quizzes + Labs
363
+
364
+ ### TASK: ASGN-01 — Assignments List
365
+
366
+ **Test first:**
367
+
368
+ ```javascript
369
+ fetch('http://localhost:8081/api/assignments?courseId=1', {
370
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
371
+ })
372
+ .then((r) => r.json())
373
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
374
+ ```
375
+
376
+ ```sql
377
+ SELECT * FROM assignments WHERE course_id = 1 LIMIT 5;
378
+ ```
379
+
380
+ ---
381
+
382
+ ### TASK: ASGN-02 — Assignment Detail
383
+
384
+ **Test first:**
385
+
386
+ ```javascript
387
+ fetch('http://localhost:8081/api/assignments/1', {
388
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
389
+ })
390
+ .then((r) => r.json())
391
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
392
+ ```
393
+
394
+ ---
395
+
396
+ ### TASK: ASGN-03 — Submit Assignment
397
+
398
+ **Test first:**
399
+
400
+ ```javascript
401
+ // Test text submission first (no file)
402
+ fetch('http://localhost:8081/api/assignments/1/submit', {
403
+ method: 'POST',
404
+ headers: {
405
+ Authorization: 'Bearer STUDENT_TOKEN',
406
+ 'Content-Type': 'application/json',
407
+ },
408
+ body: JSON.stringify({ submissionText: 'Test submission' }),
409
+ })
410
+ .then((r) => r.json())
411
+ .then(console.log);
412
+ ```
413
+
414
+ ```sql
415
+ SELECT * FROM assignment_submissions WHERE assignment_id = 1 LIMIT 5;
416
+ ```
417
+
418
+ ---
419
+
420
+ ### TASK: ASGN-04 — My Submission
421
+
422
+ **Test first:**
423
+
424
+ ```javascript
425
+ fetch('http://localhost:8081/api/assignments/1/submissions/my', {
426
+ headers: { Authorization: 'Bearer STUDENT_TOKEN' },
427
+ })
428
+ .then((r) => r.json())
429
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
430
+ ```
431
+
432
+ ---
433
+
434
+ ### TASK: ASGN-05+06 — All Submissions + Grade
435
+
436
+ **Test first:**
437
+
438
+ ```javascript
439
+ // List all submissions
440
+ fetch('http://localhost:8081/api/assignments/1/submissions', {
441
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
442
+ })
443
+ .then((r) => r.json())
444
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
445
+
446
+ // Grade a submission (find submissionId first)
447
+ fetch('http://localhost:8081/api/assignments/1/submissions/1/grade', {
448
+ method: 'PATCH',
449
+ headers: {
450
+ Authorization: 'Bearer ADMIN_TOKEN',
451
+ 'Content-Type': 'application/json',
452
+ },
453
+ body: JSON.stringify({ score: 85, feedback: 'Good work', letterGrade: 'A' }),
454
+ })
455
+ .then((r) => r.json())
456
+ .then(console.log);
457
+ ```
458
+
459
+ ---
460
+
461
+ ### TASK: QUIZ-01 — Quiz List
462
+
463
+ **Test first:**
464
+
465
+ ```javascript
466
+ fetch('http://localhost:8081/quizzes?courseId=1', {
467
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
468
+ })
469
+ .then((r) => r.json())
470
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
471
+ ```
472
+
473
+ ```sql
474
+ SELECT * FROM quizzes WHERE course_id = 1 LIMIT 5;
475
+ ```
476
+
477
+ ---
478
+
479
+ ### TASK: QUIZ-02+03 — Quiz Attempt + Submit
480
+
481
+ **Test first:**
482
+
483
+ ```javascript
484
+ // Start attempt
485
+ fetch('http://localhost:8081/quizzes/1/attempts/start', {
486
+ method: 'POST',
487
+ headers: {
488
+ Authorization: 'Bearer STUDENT_TOKEN',
489
+ 'Content-Type': 'application/json',
490
+ },
491
+ body: JSON.stringify({}),
492
+ })
493
+ .then((r) => r.json())
494
+ .then((d) => {
495
+ console.log('attempt:', JSON.stringify(d, null, 2));
496
+ window._attemptId = d.attemptId || d.id;
497
+ });
498
+
499
+ // Submit attempt (use attemptId from above)
500
+ fetch(`http://localhost:8081/quizzes/attempts/${window._attemptId}/submit`, {
501
+ method: 'POST',
502
+ headers: {
503
+ Authorization: 'Bearer STUDENT_TOKEN',
504
+ 'Content-Type': 'application/json',
505
+ },
506
+ body: JSON.stringify({ answers: [] }),
507
+ })
508
+ .then((r) => r.json())
509
+ .then(console.log);
510
+ ```
511
+
512
+ ---
513
+
514
+ ### TASK: LAB-01+02 — Labs List + Detail
515
+
516
+ **Test first:**
517
+
518
+ ```javascript
519
+ fetch('http://localhost:8081/api/labs?courseId=1', {
520
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
521
+ })
522
+ .then((r) => r.json())
523
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
524
+
525
+ fetch('http://localhost:8081/api/labs/3/instructions', {
526
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
527
+ })
528
+ .then((r) => r.json())
529
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
530
+ ```
531
+
532
+ ---
533
+
534
+ ## Phase 6 — Grades + Attendance
535
+
536
+ ### TASK: GRADE-01 — My Grades
537
+
538
+ **Test first:**
539
+
540
+ ```javascript
541
+ fetch('http://localhost:8081/api/grades/my', {
542
+ headers: { Authorization: 'Bearer STUDENT_TOKEN' },
543
+ })
544
+ .then((r) => r.json())
545
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
546
+ ```
547
+
548
+ ```sql
549
+ SELECT g.*, c.name as course_name
550
+ FROM grades g
551
+ JOIN courses c ON g.course_id = c.id
552
+ WHERE g.user_id = 57 LIMIT 10;
553
+ ```
554
+
555
+ ---
556
+
557
+ ### TASK: GRADE-02 — GPA Widget
558
+
559
+ **Test first:**
560
+
561
+ ```javascript
562
+ fetch('http://localhost:8081/api/grades/gpa/57', {
563
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
564
+ })
565
+ .then((r) => r.json())
566
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
567
+ ```
568
+
569
+ ---
570
+
571
+ ### TASK: GRADE-03 — Transcript
572
+
573
+ **Test first:**
574
+
575
+ ```javascript
576
+ fetch('http://localhost:8081/api/grades/transcript/57', {
577
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
578
+ })
579
+ .then((r) => r.json())
580
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
581
+ ```
582
+
583
+ ---
584
+
585
+ ### TASK: ATT-01+02 — Attendance Sessions
586
+
587
+ **Test first:**
588
+
589
+ ```javascript
590
+ // Create session
591
+ fetch('http://localhost:8081/attendance/sessions', {
592
+ method: 'POST',
593
+ headers: {
594
+ Authorization: 'Bearer ADMIN_TOKEN',
595
+ 'Content-Type': 'application/json',
596
+ },
597
+ body: JSON.stringify({
598
+ sectionId: 1,
599
+ sessionDate: '2026-03-15',
600
+ sessionType: 'lecture',
601
+ startTime: '09:00:00',
602
+ endTime: '10:30:00',
603
+ }),
604
+ })
605
+ .then((r) => r.json())
606
+ .then(console.log);
607
+
608
+ // List sessions
609
+ fetch('http://localhost:8081/attendance/sessions?sectionId=1', {
610
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
611
+ })
612
+ .then((r) => r.json())
613
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
614
+ ```
615
+
616
+ ```sql
617
+ SELECT * FROM attendance_sessions LIMIT 5;
618
+ DESCRIBE attendance_sessions;
619
+ ```
620
+
621
+ ---
622
+
623
+ ### TASK: ATT-03 — Batch Attendance
624
+
625
+ **Test first:**
626
+
627
+ ```javascript
628
+ fetch('http://localhost:8081/attendance/records/batch', {
629
+ method: 'POST',
630
+ headers: {
631
+ Authorization: 'Bearer ADMIN_TOKEN',
632
+ 'Content-Type': 'application/json',
633
+ },
634
+ body: JSON.stringify({
635
+ sessionId: 1,
636
+ records: [
637
+ { userId: 57, status: 'present' },
638
+ { userId: 63, status: 'absent' },
639
+ ],
640
+ }),
641
+ })
642
+ .then((r) => r.json())
643
+ .then(console.log);
644
+ ```
645
+
646
+ ```sql
647
+ DESCRIBE attendance_records;
648
+ ```
649
+
650
+ ---
651
+
652
+ ### TASK: ATT-04 — My Attendance (Student)
653
+
654
+ **Test first:**
655
+
656
+ ```javascript
657
+ fetch('http://localhost:8081/attendance/my', {
658
+ headers: { Authorization: 'Bearer STUDENT_TOKEN' },
659
+ })
660
+ .then((r) => r.json())
661
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
662
+ ```
663
+
664
+ ---
665
+
666
+ ## Phase 7 — Announcements + Discussions + Schedule
667
+
668
+ ### TASK: ANN-01+02 — Announcements
669
+
670
+ **Test first:**
671
+
672
+ ```javascript
673
+ // List
674
+ fetch('http://localhost:8081/api/announcements', {
675
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
676
+ })
677
+ .then((r) => r.json())
678
+ .then((d) => console.log(JSON.stringify(d[0], null, 2)));
679
+
680
+ // Create
681
+ fetch('http://localhost:8081/api/announcements', {
682
+ method: 'POST',
683
+ headers: {
684
+ Authorization: 'Bearer ADMIN_TOKEN',
685
+ 'Content-Type': 'application/json',
686
+ },
687
+ body: JSON.stringify({
688
+ title: 'Test Announcement',
689
+ content: 'Test content',
690
+ courseId: 1,
691
+ priority: 'medium',
692
+ }),
693
+ })
694
+ .then((r) => r.json())
695
+ .then(console.log);
696
+ ```
697
+
698
+ ---
699
+
700
+ ### TASK: DISC-01+02 — Discussions
701
+
702
+ **Test first:**
703
+
704
+ ```javascript
705
+ fetch('http://localhost:8081/api/discussions?courseId=1', {
706
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
707
+ })
708
+ .then((r) => r.json())
709
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
710
+
711
+ fetch('http://localhost:8081/api/discussions/3', {
712
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
713
+ })
714
+ .then((r) => r.json())
715
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
716
+ ```
717
+
718
+ ---
719
+
720
+ ### TASK: SCHED-01+02 — Schedule
721
+
722
+ **Test first:**
723
+
724
+ ```javascript
725
+ fetch('http://localhost:8081/api/schedule/my/daily', {
726
+ headers: { Authorization: 'Bearer STUDENT_TOKEN' },
727
+ })
728
+ .then((r) => r.json())
729
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
730
+
731
+ fetch('http://localhost:8081/api/schedule/my/weekly', {
732
+ headers: { Authorization: 'Bearer STUDENT_TOKEN' },
733
+ })
734
+ .then((r) => r.json())
735
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
736
+ ```
737
+
738
+ ---
739
+
740
+ ## Phase 9 — Notifications + Files + Community
741
+
742
+ ### TASK: NOTIF-01+02 — Notifications
743
+
744
+ **Test first:**
745
+
746
+ ```javascript
747
+ fetch('http://localhost:8081/api/notifications/unread-count', {
748
+ headers: { Authorization: 'Bearer STUDENT_TOKEN' },
749
+ })
750
+ .then((r) => r.json())
751
+ .then(console.log);
752
+
753
+ fetch('http://localhost:8081/api/notifications', {
754
+ headers: { Authorization: 'Bearer STUDENT_TOKEN' },
755
+ })
756
+ .then((r) => r.json())
757
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
758
+ ```
759
+
760
+ ```sql
761
+ SELECT * FROM notifications WHERE user_id = 57 LIMIT 5;
762
+ DESCRIBE notifications;
763
+ ```
764
+
765
+ ---
766
+
767
+ ### TASK: FILES-01+02 — Files
768
+
769
+ **Test first:**
770
+
771
+ ```javascript
772
+ fetch('http://localhost:8081/api/files/recent', {
773
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
774
+ })
775
+ .then((r) => r.json())
776
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
777
+
778
+ fetch('http://localhost:8081/api/files/folders/1', {
779
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
780
+ })
781
+ .then((r) => r.json())
782
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
783
+ ```
784
+
785
+ ```sql
786
+ SELECT * FROM files LIMIT 5;
787
+ SELECT * FROM folders LIMIT 5;
788
+ DESCRIBE files;
789
+ ```
790
+
791
+ ---
792
+
793
+ ### TASK: COMM-01+02 — Community
794
+
795
+ **Test first:**
796
+
797
+ ```javascript
798
+ fetch('http://localhost:8081/api/community/global', {
799
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
800
+ })
801
+ .then((r) => r.json())
802
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
803
+
804
+ fetch('http://localhost:8081/api/community/communities', {
805
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
806
+ })
807
+ .then((r) => r.json())
808
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
809
+ ```
810
+
811
+ ---
812
+
813
+ ## Phase 10 — Analytics + Reports + Search
814
+
815
+ ### TASK: ANAL-01 — Analytics Dashboard
816
+
817
+ **Test first:**
818
+
819
+ ```javascript
820
+ fetch('http://localhost:8081/api/analytics/dashboard', {
821
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
822
+ })
823
+ .then((r) => r.json())
824
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
825
+
826
+ fetch('http://localhost:8081/api/analytics/courses/1', {
827
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
828
+ })
829
+ .then((r) => r.json())
830
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
831
+ ```
832
+
833
+ ---
834
+
835
+ ### TASK: SEARCH-01 — Global Search
836
+
837
+ **Test first:**
838
+
839
+ ```javascript
840
+ fetch('http://localhost:8081/api/search?query=programming&page=1&limit=10', {
841
+ headers: { Authorization: 'Bearer ADMIN_TOKEN' },
842
+ })
843
+ .then((r) => r.json())
844
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
845
+ ```
846
+
847
+ ```sql
848
+ SELECT * FROM search_index LIMIT 5;
849
+ ```
850
+
851
+ ---
852
+
853
+ ## Phase 11 — Tasks + Google Drive + YouTube
854
+
855
+ ### TASK: TASKS-01+02 — Tasks
856
+
857
+ **Test first:**
858
+
859
+ ```javascript
860
+ fetch('http://localhost:8081/api/tasks?page=1&limit=10', {
861
+ headers: { Authorization: 'Bearer STUDENT_TOKEN' },
862
+ })
863
+ .then((r) => r.json())
864
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
865
+
866
+ fetch('http://localhost:8081/api/tasks/upcoming?days=7', {
867
+ headers: { Authorization: 'Bearer STUDENT_TOKEN' },
868
+ })
869
+ .then((r) => r.json())
870
+ .then((d) => console.log(JSON.stringify(d, null, 2)));
871
+ ```
872
+
873
+ ```sql
874
+ SELECT * FROM student_tasks WHERE user_id = 57 LIMIT 5;
875
+ DESCRIBE student_tasks;
876
+ ```
877
+
878
+ ---
879
+
880
+ ## HOW TO USE THIS PLAN
881
+
882
+ For each task:
883
+
884
+ 1. **Run the test snippets** in the browser console
885
+ 2. **Run the SQL queries** in your MySQL client
886
+ 3. **Share the output** here — I'll give you an exact Copilot prompt with real field names
887
+ 4. **Paste the prompt** into Copilot Chat and implement
888
+ 5. **Mark the task done** and move to the next one
889
+
890
+ Start with **COURSE-03** (My Courses Sidebar). Run the test, share the output.
891
+
892
+ ---
893
+
894
+ ## Test Accounts
895
+
896
+ | Role | Email | Password |
897
+ | ---------- | ---------------------------- | ------------- |
898
+ | Admin | admin.tarek@example.com | SecureP@ss123 |
899
+ | Instructor | instructor.tarek@example.com | SecureP@ss123 |
900
+ | Student | student.tarek@example.com | SecureP@ss123 |
901
+ | TA | ta.tarek@example.com | SecureP@ss123 |
902
+
903
+ ## Endpoint Prefix Watch ⚠️
904
+
905
+ These do NOT use `/api/` prefix:
906
+
907
+ - Quizzes → `/quizzes/`
908
+ - Attendance → `/attendance/`
909
+ - YouTube → `/youtube/`
910
+ - Google Drive → `/google-drive/`
integrationPlan.md ADDED
@@ -0,0 +1,445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # EduVerse Frontend Integration Master Plan — Updated
2
+ **Last Updated**: 2026-03-15
3
+ **Stack**: React (Vite) + NestJS Backend | **Base URL**: `http://localhost:8081`
4
+
5
+ ---
6
+
7
+ ## ✅ COMPLETED
8
+
9
+ - Phase 0 — Infrastructure (Axios, AuthContext, ProtectedRoute, error handling, pagination)
10
+ - Phase 1 — Auth + Layout (login, logout, refresh, role routing, all 4 dashboard shells, sidebar, header, profile, settings)
11
+ - Phase 8 — Messaging (full WebSocket chat on all tabs, Socket.IO, fallback polling)
12
+ - Phase 2 — Semester fix (active semester, date range fix in backend)
13
+ - Course Management — Create course (multi-step: course + section + schedule + staff), course cards rendering, instructor/TA assignment + display (with new backend endpoints GET /section/:id/instructor and /tas)
14
+
15
+ ---
16
+
17
+ ## REMAINING API MAPPING
18
+
19
+ ### Admin — User Management (`/api/admin`) — NOT YET INTEGRATED
20
+
21
+ | Method | Endpoint | Component | Roles |
22
+ |--------|----------|-----------|-------|
23
+ | GET | `/api/admin/users` | `<UserManagementPage />` | admin, it_admin |
24
+ | GET | `/api/admin/users/search` | `<UserSearchBar />` | admin, it_admin |
25
+ | GET | `/api/admin/users/statistics` | `<AdminDashboard />` | admin, it_admin |
26
+ | GET | `/api/admin/users/:id` | `<UserDetailModal />` | admin, it_admin |
27
+ | PUT | `/api/admin/users/:id` | `<EditUserForm />` | admin, it_admin |
28
+ | PUT | `/api/admin/users/:id/status` | `<UserStatusToggle />` | admin, it_admin |
29
+ | GET | `/api/admin/roles` | `<RolesListPage />` | admin, it_admin |
30
+ | POST | `/api/admin/roles` | `<CreateRoleForm />` | it_admin only |
31
+ | GET | `/api/admin/permissions/matrix` | `<PermissionsMatrix />` | admin, it_admin |
32
+
33
+ ### Campus (`/api/campuses`, `/api/departments`, `/api/programs`) — NOT YET INTEGRATED
34
+
35
+ | Method | Endpoint | Component | Roles |
36
+ |--------|----------|-----------|-------|
37
+ | GET | `/api/campuses` | `<CampusListPage />` | Any |
38
+ | POST | `/api/campuses` | `<CreateCampusForm />` | admin, it_admin |
39
+ | PUT | `/api/campuses/:id` | `<EditCampusForm />` | admin, it_admin |
40
+ | GET | `/api/campuses/:id/departments` | `<DepartmentList />` | Any |
41
+ | POST | `/api/departments` | `<CreateDeptForm />` | admin, it_admin |
42
+ | GET | `/api/departments/:id/programs` | `<ProgramList />` | Any |
43
+ | POST | `/api/programs` | `<CreateProgramForm />` | admin, it_admin |
44
+
45
+ ### Enrollments (`/api/enrollments`) — PARTIALLY DONE
46
+
47
+ | Method | Endpoint | Component | Roles | Status |
48
+ |--------|----------|-----------|-------|--------|
49
+ | GET | `/api/enrollments/my-courses` | `<MyCoursesSidebar />` | student | ❌ TODO |
50
+ | GET | `/api/enrollments/available` | `<AvailableCoursesPage />` | student | ❌ TODO |
51
+ | POST | `/api/enrollments/register` | `<EnrollButton />` | student | ❌ TODO |
52
+ | DELETE | `/api/enrollments/:id` | `<DropCourseButton />` | student | ❌ TODO |
53
+ | GET | `/api/enrollments/section/:id/students` | `<StudentRosterTable />` | instructor, admin | ❌ TODO |
54
+ | GET | `/api/enrollments/section/:id/waitlist` | `<WaitlistTable />` | instructor, admin | ❌ TODO |
55
+ | POST | `/api/enrollments/assign-instructor` | multi-step modal | admin | ✅ DONE |
56
+ | POST | `/api/enrollments/assign-ta` | multi-step modal | admin | ✅ DONE |
57
+ | GET | `/api/enrollments/section/:id/instructor` | course card display | admin | ✅ DONE |
58
+ | GET | `/api/enrollments/section/:id/tas` | course card display | admin | ✅ DONE |
59
+
60
+ ### Course Materials (`/api/courses/:id/materials`) — NOT YET
61
+
62
+ | Method | Endpoint | Component | Roles |
63
+ |--------|----------|-----------|-------|
64
+ | GET | `/api/courses/:id/materials` | `<MaterialsList />` | Any |
65
+ | POST | `/api/courses/:id/materials` | `<CreateMaterialForm />` | instructor, ta, admin |
66
+ | POST | `/api/courses/:id/materials/video` | `<VideoUploadModal />` | instructor, ta, admin |
67
+ | PUT | `/api/courses/:id/materials/:matId` | `<EditMaterialForm />` | instructor, ta, admin |
68
+ | DELETE | `/api/courses/:id/materials/:matId` | `<DeleteMaterialButton />` | instructor, admin |
69
+ | PATCH | `/api/courses/:id/materials/:matId/visibility` | `<VisibilityToggle />` | instructor, ta, admin |
70
+ | GET | `/api/courses/:id/materials/:matId/download` | `<DownloadButton />` | Any |
71
+ | GET | `/api/courses/:id/materials/:matId/embed` | `<VideoPlayer />` | Any |
72
+ | GET | `/api/courses/:id/structure` | `<CourseStructureView />` | Any |
73
+ | POST | `/api/courses/:id/structure` | `<AddWeekForm />` | instructor, admin |
74
+
75
+ ### Assignments (`/api/assignments`) — NOT YET
76
+
77
+ | Method | Endpoint | Component | Roles |
78
+ |--------|----------|-----------|-------|
79
+ | GET | `/api/assignments?courseId=X` | `<AssignmentsList />` | Any |
80
+ | POST | `/api/assignments` | `<CreateAssignmentForm />` | instructor, admin |
81
+ | PATCH | `/api/assignments/:id` | `<EditAssignmentForm />` | instructor, admin |
82
+ | POST | `/api/assignments/:id/submit` | `<SubmitAssignmentForm />` | student |
83
+ | GET | `/api/assignments/:id/submissions/my` | `<MySubmissionView />` | student |
84
+ | GET | `/api/assignments/:id/submissions` | `<AllSubmissionsTable />` | instructor, ta, admin |
85
+ | PATCH | `/api/assignments/:id/submissions/:subId/grade` | `<GradeSubmissionForm />` | instructor, ta |
86
+
87
+ ### Quizzes (`/quizzes`) — NOT YET
88
+
89
+ | Method | Endpoint | Component | Roles |
90
+ |--------|----------|-----------|-------|
91
+ | GET | `/quizzes?courseId=X` | `<QuizList />` | Any |
92
+ | POST | `/quizzes` | `<CreateQuizForm />` | instructor, ta, admin |
93
+ | GET | `/quizzes/:id` | `<QuizDetailPage />` | Any |
94
+ | POST | `/quizzes/:quizId/attempts/start` | `<StartQuizButton />` | student |
95
+ | POST | `/quizzes/attempts/:attemptId/submit` | `<QuizSubmitButton />` | student |
96
+ | GET | `/quizzes/my-attempts` | `<MyQuizAttemptsPage />` | student |
97
+ | GET | `/quizzes/attempts` | `<AllAttemptsTable />` | instructor, ta, admin |
98
+ | GET | `/quizzes/:quizId/statistics` | `<QuizStatisticsChart />` | instructor, admin |
99
+ | POST | `/quizzes/:quizId/questions` | `<AddQuestionForm />` | instructor, ta, admin |
100
+
101
+ ### Labs (`/api/labs`) — NOT YET
102
+
103
+ | Method | Endpoint | Component | Roles |
104
+ |--------|----------|-----------|-------|
105
+ | GET | `/api/labs?courseId=X` | `<LabsList />` | Any |
106
+ | POST | `/api/labs` | `<CreateLabForm />` | instructor, ta, admin |
107
+ | GET | `/api/labs/:id/instructions` | `<LabInstructionsView />` | Any |
108
+ | POST | `/api/labs/:id/submit` | `<SubmitLabForm />` | student |
109
+ | GET | `/api/labs/:id/submissions/my` | `<MyLabSubmissionView />` | student |
110
+ | GET | `/api/labs/:id/submissions` | `<LabSubmissionsTable />` | instructor, ta, admin |
111
+ | POST | `/api/labs/:id/attendance` | `<MarkLabAttendanceForm />` | instructor, ta, admin |
112
+
113
+ ### Grades (`/api/grades`) — NOT YET
114
+
115
+ | Method | Endpoint | Component | Roles |
116
+ |--------|----------|-----------|-------|
117
+ | GET | `/api/grades/my` | `<MyGradesPage />` | student |
118
+ | GET | `/api/grades/gpa/:studentId` | `<GPAWidget />` | student, admin |
119
+ | GET | `/api/grades/transcript/:studentId` | `<TranscriptView />` | student, admin |
120
+ | GET | `/api/grades?courseId=X` | `<GradebookTable />` | instructor, ta, admin |
121
+ | GET | `/api/grades/distribution/:courseId` | `<GradeDistributionChart />` | instructor, admin |
122
+ | PUT | `/api/grades/:id` | `<EditGradeForm />` | instructor, ta |
123
+ | GET | `/api/rubrics?courseId=X` | `<RubricsList />` | instructor, ta |
124
+ | POST | `/api/rubrics` | `<CreateRubricForm />` | instructor |
125
+
126
+ ### Attendance (`/attendance`) — NOT YET
127
+
128
+ | Method | Endpoint | Component | Roles |
129
+ |--------|----------|-----------|-------|
130
+ | POST | `/attendance/sessions` | `<CreateSessionForm />` | instructor, ta, admin |
131
+ | GET | `/attendance/sessions?sectionId=X` | `<AttendanceSessionsList />` | instructor, ta, admin |
132
+ | POST | `/attendance/records/batch` | `<BatchAttendanceForm />` | instructor, ta, admin |
133
+ | GET | `/attendance/my` | `<MyAttendancePage />` | student |
134
+ | GET | `/attendance/by-course/:courseId` | `<CourseAttendancePage />` | instructor, ta, admin |
135
+ | GET | `/attendance/summary/:sectionId` | `<AttendanceSummaryChart />` | instructor, ta, admin |
136
+ | POST | `/attendance/import-excel` | `<ImportAttendanceButton />` | instructor, admin |
137
+
138
+ ### Announcements (`/api/announcements`) — NOT YET
139
+
140
+ | Method | Endpoint | Component | Roles |
141
+ |--------|----------|-----------|-------|
142
+ | GET | `/api/announcements` | `<AnnouncementsList />` | Any |
143
+ | POST | `/api/announcements` | `<CreateAnnouncementForm />` | instructor, ta, admin |
144
+ | PUT | `/api/announcements/:id` | `<EditAnnouncementForm />` | instructor, ta, admin |
145
+ | PATCH | `/api/announcements/:id/publish` | `<PublishButton />` | instructor, admin |
146
+ | PATCH | `/api/announcements/:id/pin` | `<PinToggle />` | instructor, admin |
147
+
148
+ ### Discussions (`/api/discussions`) — NOT YET
149
+
150
+ | Method | Endpoint | Component | Roles |
151
+ |--------|----------|-----------|-------|
152
+ | GET | `/api/discussions?courseId=X` | `<DiscussionsList />` | Any |
153
+ | POST | `/api/discussions` | `<CreateThreadForm />` | Any |
154
+ | GET | `/api/discussions/:id` | `<ThreadDetailPage />` | Any |
155
+ | POST | `/api/discussions/:id/reply` | `<ReplyForm />` | Any |
156
+ | PATCH | `/api/discussions/:id/pin` | `<PinThreadButton />` | instructor, ta, admin |
157
+ | PATCH | `/api/discussions/:id/lock` | `<LockThreadButton />` | instructor, ta, admin |
158
+
159
+ ### Community (`/api/community`) — NOT YET
160
+
161
+ | Method | Endpoint | Component | Roles |
162
+ |--------|----------|-----------|-------|
163
+ | GET | `/api/community/global` | `<GlobalFeedPage />` | Any |
164
+ | GET | `/api/community/communities` | `<CommunitiesList />` | Any |
165
+ | GET | `/api/community/communities/:id/posts` | `<CommunityPostsFeed />` | Any |
166
+ | POST | `/api/community/communities/:id/posts` | `<CreatePostForm />` | Any |
167
+ | POST | `/api/community/posts/:id/comment` | `<CommentForm />` | Any |
168
+ | POST | `/api/community/posts/:id/react` | `<ReactionButton />` | Any |
169
+
170
+ ### Files (`/api/files`) — NOT YET
171
+
172
+ | Method | Endpoint | Component | Roles |
173
+ |--------|----------|-----------|-------|
174
+ | POST | `/api/files/upload` | `<FileUploadDropzone />` | Any |
175
+ | GET | `/api/files/recent` | `<RecentFilesWidget />` | Any |
176
+ | GET | `/api/files/shared` | `<SharedFilesPage />` | Any |
177
+ | GET | `/api/files/search` | `<FileSearchPage />` | Any |
178
+ | GET | `/api/files/:fileId/download` | `<DownloadFileButton />` | Any |
179
+ | GET | `/api/files/folders/:id/children` | `<FolderBrowser />` | Any |
180
+ | POST | `/api/files/folders` | `<CreateFolderForm />` | Any |
181
+
182
+ ### Notifications (`/api/notifications`) — NOT YET
183
+
184
+ | Method | Endpoint | Component | Roles |
185
+ |--------|----------|-----------|-------|
186
+ | GET | `/api/notifications/unread-count` | `<BellIcon />` | Any |
187
+ | GET | `/api/notifications` | `<NotificationsDropdown />` | Any |
188
+ | PATCH | `/api/notifications/read-all` | `<MarkAllReadButton />` | Any |
189
+ | GET | `/api/notifications/preferences` | `<NotificationSettingsPage />` | Any |
190
+ | PUT | `/api/notifications/preferences` | `<NotificationSettingsForm />` | Any |
191
+ | POST | `/api/notifications/send` | `<SendNotificationForm />` | admin, it_admin |
192
+
193
+ ### Schedule & Calendar — NOT YET
194
+
195
+ | Method | Endpoint | Component | Roles |
196
+ |--------|----------|-----------|-------|
197
+ | GET | `/api/schedule/my/daily` | `<DailyScheduleWidget />` | Any |
198
+ | GET | `/api/schedule/my/weekly` | `<WeeklyCalendarView />` | Any |
199
+ | GET | `/api/calendar/events` | `<PersonalEventsList />` | Any |
200
+ | POST | `/api/calendar/events` | `<CreateEventForm />` | Any |
201
+ | GET | `/api/exams/schedule` | `<ExamSchedulePage />` | Any |
202
+ | POST | `/api/exams/schedule` | `<CreateExamForm />` | instructor, admin |
203
+
204
+ ### Analytics & Reports — NOT YET
205
+
206
+ | Method | Endpoint | Component | Roles |
207
+ |--------|----------|-----------|-------|
208
+ | GET | `/api/analytics/dashboard` | `<AdminAnalyticsDashboard />` | admin |
209
+ | GET | `/api/analytics/performance?courseId=X` | `<PerformanceChart />` | admin, instructor |
210
+ | GET | `/api/analytics/courses/:courseId` | `<CourseAnalyticsPage />` | instructor |
211
+ | GET | `/api/analytics/students/:studentId` | `<StudentAnalyticsPage />` | student, admin |
212
+ | GET | `/api/analytics/at-risk-students` | `<AtRiskStudentsAlert />` | instructor, admin |
213
+ | GET | `/api/reports/templates` | `<ReportTemplatesList />` | admin |
214
+ | POST | `/api/reports/generate` | `<GenerateReportForm />` | admin |
215
+ | GET | `/api/reports/history` | `<ReportHistoryPage />` | admin |
216
+
217
+ ### Search (`/api/search`) — NOT YET
218
+
219
+ | Method | Endpoint | Component | Roles |
220
+ |--------|----------|-----------|-------|
221
+ | GET | `/api/search?query=X` | `<GlobalSearchPage />` | Any |
222
+ | GET | `/api/search/courses` | `<CourseSearchResults />` | Any |
223
+ | GET | `/api/search/users` | `<UserSearchResults />` | Any |
224
+ | GET | `/api/search/materials` | `<MaterialSearchResults />` | Any |
225
+
226
+ ### Tasks & Reminders (`/api/tasks`) — NOT YET
227
+
228
+ | Method | Endpoint | Component | Roles |
229
+ |--------|----------|-----------|-------|
230
+ | GET | `/api/tasks?page=1&limit=20` | `<TasksListPage />` | Any |
231
+ | POST | `/api/tasks` | `<CreateTaskForm />` | Any |
232
+ | GET | `/api/tasks/upcoming?days=7` | `<UpcomingTasksWidget />` | Any |
233
+ | PATCH | `/api/tasks/:id` | `<EditTaskForm />` | Any |
234
+ | POST | `/api/tasks/:id/complete` | `<CompleteTaskButton />` | Any |
235
+ | POST | `/api/tasks/reminders` | `<SetReminderForm />` | Any |
236
+
237
+ ---
238
+
239
+ ## REMAINING PHASES
240
+
241
+ ### Next Up — Phase 3 Completion: Student Enrollment Flow
242
+
243
+ **Goal**: Student can browse courses, enroll, see My Courses sidebar, view roster.
244
+
245
+ - [ ] COURSE-03 `<MyCoursesSidebar />` — GET `/api/enrollments/my-courses`
246
+ - [ ] COURSE-04 `<AvailableCoursesPage />` — GET `/api/enrollments/available`
247
+ - [ ] COURSE-05 `<EnrollButton />` with section picker modal — POST `/api/enrollments/register`
248
+ - [ ] COURSE-06 `<DropCourseButton />` — DELETE `/api/enrollments/:id`
249
+ - [ ] COURSE-07 `<StudentRosterTable />` — GET `/api/enrollments/section/:sectionId/students`
250
+ - [ ] COURSE-08 `<WaitlistTable />` — GET `/api/enrollments/section/:sectionId/waitlist`
251
+ - [ ] COURSE-09 `<SectionScheduleView />` — GET `/api/schedules/section/:sectionId`
252
+
253
+ ---
254
+
255
+ ### Phase 4 — Course Materials + Structure
256
+
257
+ **Goal**: Instructor uploads materials (files + YouTube). Student views/downloads.
258
+
259
+ - [ ] MAT-01 `<CourseStructureView />` — GET `/api/courses/:id/structure`
260
+ - [ ] MAT-02 `<MaterialsList />` — GET `/api/courses/:id/materials` (student: published only)
261
+ - [ ] MAT-03 `<VideoPlayer />` — renders YouTube iframe from `externalUrl`
262
+ - [ ] MAT-04 `<DownloadButton />` — GET `/api/courses/:id/materials/:matId/download`
263
+ - [ ] MAT-05 `<FileUploadModal />` — POST with field `file`, show progress bar
264
+ - [ ] MAT-06 `<VideoUploadModal />` — POST with field `video`, YouTube upload
265
+ - [ ] MAT-07 `<VisibilityToggle />` — PATCH visibility `{ isPublished: bool }`
266
+ - [ ] MAT-08 `<EditMaterialForm />` — PUT material
267
+ - [ ] MAT-09 `<AddWeekForm />` — POST `/api/courses/:id/structure`
268
+
269
+ ---
270
+
271
+ ### Phase 5 — Assignments + Quizzes + Labs
272
+
273
+ **Goal**: Full assessment workflow. Students submit. Instructors grade.
274
+
275
+ #### Assignments
276
+ - [ ] ASGN-01 `<AssignmentsList />` — sorted by dueDate, badge for overdue
277
+ - [ ] ASGN-02 `<AssignmentDetailPage />` — instructions + due date + max score
278
+ - [ ] ASGN-03 `<SubmitAssignmentForm />` — multipart with optional file
279
+ - [ ] ASGN-04 `<MySubmissionView />` — show grade + feedback if graded
280
+ - [ ] ASGN-05 `<AllSubmissionsTable />` — paginated, sortable by grade
281
+ - [ ] ASGN-06 `<GradeSubmissionForm />` — score + feedback + letterGrade
282
+ - [ ] ASGN-07 `<CreateAssignmentForm />` — POST with courseId, title, dueDate, maxScore
283
+
284
+ #### Quizzes
285
+ - [ ] QUIZ-01 `<QuizList />` — show availability window, attempts remaining
286
+ - [ ] QUIZ-02 `<QuizAttemptPage />` — timed, countdown timer, auto-submit
287
+ - [ ] QUIZ-03 `<QuizResultPage />` — score + answers if allowed
288
+ - [ ] QUIZ-04 `<QuizBuilderPage />` — create quiz + add questions
289
+ - [ ] QUIZ-05 `<QuizStatisticsChart />` — Chart.js bar chart
290
+ - [ ] QUIZ-06 `<MyQuizAttemptsPage />` — GET `/quizzes/my-attempts`
291
+
292
+ #### Labs
293
+ - [ ] LAB-01 `<LabsList />` — per course
294
+ - [ ] LAB-02 `<LabDetailPage />` — instructions view
295
+ - [ ] LAB-03 `<SubmitLabForm />` — text + optional file
296
+ - [ ] LAB-04 `<MyLabSubmissionView />` — student view
297
+ - [ ] LAB-05 `<LabSubmissionsTable />` + `<GradeLabForm />`
298
+ - [ ] LAB-06 `<MarkLabAttendanceForm />` — present/absent per student
299
+
300
+ ---
301
+
302
+ ### Phase 6 — Grades + Attendance + Rubrics
303
+
304
+ #### Grades
305
+ - [ ] GRADE-01 `<MyGradesPage />` — grouped by course, letter grade + score
306
+ - [ ] GRADE-02 `<GPAWidget />` — on student dashboard home
307
+ - [ ] GRADE-03 `<TranscriptView />` — full academic history
308
+ - [ ] GRADE-04 `<GradebookTable />` — editable cells for instructor
309
+ - [ ] GRADE-05 `<GradeDistributionChart />` — Chart.js histogram
310
+ - [ ] GRADE-06 `<EditGradeForm />` — PUT `/api/grades/:id`
311
+ - [ ] GRADE-07 `<CreateRubricForm />` — POST `/api/rubrics`
312
+ - [ ] GRADE-08 `<RubricsList />` — GET `/api/rubrics?courseId=X`
313
+
314
+ #### Attendance
315
+ - [ ] ATT-01 `<CreateSessionForm />` — POST `/attendance/sessions`
316
+ - [ ] ATT-02 `<AttendanceSessionsList />` — GET sessions by section
317
+ - [ ] ATT-03 `<BatchAttendanceForm />` — present/absent/late toggles per student
318
+ - [ ] ATT-04 `<MyAttendancePage />` — per-course attendance percentage
319
+ - [ ] ATT-05 `<CourseAttendancePage />` — instructor view
320
+ - [ ] ATT-06 `<AttendanceSummaryChart />` — Chart.js line chart
321
+ - [ ] ATT-07 `<ImportAttendanceButton />` — Excel file upload
322
+ - [ ] ATT-08 `<ExportAttendanceButton />` — download Excel
323
+
324
+ ---
325
+
326
+ ### Phase 7 — Announcements + Discussions + Schedule
327
+
328
+ #### Announcements
329
+ - [ ] ANN-01 `<AnnouncementsList />` — pinned first, role-filtered
330
+ - [ ] ANN-02 `<CreateAnnouncementForm />` — rich text editor
331
+ - [ ] ANN-03 `<PinToggle />` — PATCH pin
332
+ - [ ] ANN-04 `<PublishButton />` — PATCH publish
333
+
334
+ #### Discussions
335
+ - [ ] DISC-01 `<DiscussionsList />` — per course, pinned first
336
+ - [ ] DISC-02 `<ThreadDetailPage />` — thread + replies
337
+ - [ ] DISC-03 `<ReplyForm />` — inline reply
338
+ - [ ] DISC-04 `<PinThreadButton />` + `<LockThreadButton />`
339
+
340
+ #### Schedule
341
+ - [ ] SCHED-01 `<DailyScheduleWidget />` — on dashboard home
342
+ - [ ] SCHED-02 `<WeeklyCalendarView />` — FullCalendar
343
+ - [ ] SCHED-03 `<CalendarPage />` — schedule + personal events + exams
344
+ - [ ] SCHED-04 `<ExamSchedulePage />` — sorted by date
345
+ - [ ] SCHED-05 `<CreateExamForm />` — POST `/api/exams/schedule`
346
+
347
+ ---
348
+
349
+ ### Phase 9 — Notifications + Files + Community
350
+
351
+ #### Notifications
352
+ - [ ] NOTIF-01 `<BellIcon />` — poll unread-count every 30s
353
+ - [ ] NOTIF-02 `<NotificationsDropdown />` — click navigates by relatedEntityType
354
+ - [ ] NOTIF-03 `<NotificationsPage />` — full paginated list
355
+ - [ ] NOTIF-04 `<MarkAllReadButton />`
356
+ - [ ] NOTIF-05 `<NotificationSettingsPage />`
357
+ - [ ] NOTIF-06 `<SendNotificationForm />` — admin only
358
+
359
+ #### Files
360
+ - [ ] FILES-01 `<FilesPage />` — tabs: Recent / Shared / Search / Folders
361
+ - [ ] FILES-02 `<RecentFilesWidget />` — on dashboard
362
+ - [ ] FILES-03 `<FileUploadDropzone />` — drag-and-drop, progress bar
363
+ - [ ] FILES-04 `<FolderBrowser />` — breadcrumb navigation
364
+ - [ ] FILES-05 `<CreateFolderForm />`
365
+ - [ ] FILES-06 `<DownloadFileButton />`
366
+
367
+ #### Community
368
+ - [ ] COMM-01 `<CommunityPage />` — Global Feed + Department Communities tabs
369
+ - [ ] COMM-02 `<PostDetailPage />` — post + comments + reactions
370
+ - [ ] COMM-03 `<CreatePostForm />` — rich text + tag selector
371
+ - [ ] COMM-04 `<CommentForm />`
372
+ - [ ] COMM-05 `<ReactionButton />` — toggle like
373
+
374
+ ---
375
+
376
+ ### Phase 10 — Analytics + Reports + Search
377
+
378
+ - [ ] ANAL-01 `<AdminAnalyticsDashboard />` — system-wide stats cards
379
+ - [ ] ANAL-02 `<PerformanceChart />` — Chart.js line chart
380
+ - [ ] ANAL-03 `<InstructorAnalyticsDashboard />` — course-specific
381
+ - [ ] ANAL-04 `<StudentAnalyticsPage />` — personal progress
382
+ - [ ] ANAL-05 `<AtRiskStudentsAlert />` — warning table
383
+ - [ ] REPORT-01 `<ReportTemplatesList />`
384
+ - [ ] REPORT-02 `<GenerateReportForm />`
385
+ - [ ] REPORT-03 `<ReportHistoryPage />` + `<DownloadReportButton />`
386
+ - [ ] SEARCH-01 `<GlobalSearchPage />` — tabbed results
387
+ - [ ] SEARCH-02 Wire `<GlobalSearchBar />` in header
388
+
389
+ ---
390
+
391
+ ### Phase 11 — Tasks + Google Drive + YouTube
392
+
393
+ - [ ] TASKS-01 `<TasksListPage />` — filter by status/priority
394
+ - [ ] TASKS-02 `<CreateTaskForm />`
395
+ - [ ] TASKS-03 `<UpcomingTasksWidget />` — dashboard widget
396
+ - [ ] TASKS-04 `<EditTaskForm />`
397
+ - [ ] TASKS-05 `<CompleteTaskButton />`
398
+ - [ ] TASKS-06 `<SetReminderForm />`
399
+ - [ ] DRIVE-01 `<GoogleDriveConnectPage />` — OAuth flow
400
+ - [ ] DRIVE-02 `<DriveFolderBrowser />`
401
+ - [ ] YT-01 `<YouTubeSetupPage />` — admin one-time OAuth
402
+ - [ ] YT-02 `<YouTubeSearchModal />`
403
+
404
+ ---
405
+
406
+ ### Phase 12 — Polish + Accessibility
407
+
408
+ - [ ] Loading skeletons on all data-fetching components
409
+ - [ ] Empty state components for zero-data cases
410
+ - [ ] Error boundaries on all route-level pages
411
+ - [ ] Form validation errors inline
412
+ - [ ] Mobile responsive — sidebar collapse on <768px
413
+ - [ ] Dark mode — respect prefers-color-scheme
414
+ - [ ] Accessibility — ARIA labels, keyboard navigation
415
+ - [ ] E2E test each role's critical path
416
+
417
+ ---
418
+
419
+ ## Known Backend Gaps
420
+
421
+ | Gap | Workaround |
422
+ |-----|-----------|
423
+ | No auto-notifications on assignment/grade | Admin sends manually |
424
+ | Notifications not real-time | Poll every 30s |
425
+ | YouTube OAuth must be set up first | Show warning in VideoUploadModal |
426
+ | No student progress % | Calculate client-side |
427
+ | Calendar not auto-populated | Merge schedule API into FullCalendar |
428
+
429
+ ## Test Accounts
430
+
431
+ | Role | Email | Password |
432
+ |------|-------|----------|
433
+ | Admin | admin.tarek@example.com | SecureP@ss123 |
434
+ | Instructor | instructor.tarek@example.com | SecureP@ss123 |
435
+ | Student | student.tarek@example.com | SecureP@ss123 |
436
+ | TA | ta.tarek@example.com | SecureP@ss123 |
437
+ | IT Admin | it_admin.tarek@example.com | SecureP@ss123 |
438
+
439
+ ## Endpoint Prefix Watch ⚠️
440
+
441
+ These do NOT use `/api/` prefix:
442
+ - Quizzes → `/quizzes/`
443
+ - Attendance → `/attendance/`
444
+ - YouTube → `/youtube/`
445
+ - Google Drive → `/google-drive/`
src/modules/assignments/assignments.module.ts CHANGED
@@ -10,6 +10,7 @@ import { User } from '../auth/entities/user.entity';
10
  import { CoursesModule } from '../courses/courses.module';
11
  import { EnrollmentsModule } from '../enrollments/enrollments.module';
12
  import { GoogleDriveModule } from '../google-drive/google-drive.module';
 
13
 
14
  @Module({
15
  imports: [
@@ -24,6 +25,7 @@ import { GoogleDriveModule } from '../google-drive/google-drive.module';
24
  forwardRef(() => CoursesModule),
25
  forwardRef(() => EnrollmentsModule),
26
  GoogleDriveModule,
 
27
  ],
28
  controllers: [AssignmentsController],
29
  providers: [AssignmentsService],
 
10
  import { CoursesModule } from '../courses/courses.module';
11
  import { EnrollmentsModule } from '../enrollments/enrollments.module';
12
  import { GoogleDriveModule } from '../google-drive/google-drive.module';
13
+ import { GradesModule } from '../grades/grades.module';
14
 
15
  @Module({
16
  imports: [
 
25
  forwardRef(() => CoursesModule),
26
  forwardRef(() => EnrollmentsModule),
27
  GoogleDriveModule,
28
+ forwardRef(() => GradesModule),
29
  ],
30
  controllers: [AssignmentsController],
31
  providers: [AssignmentsService],
src/modules/assignments/entities/assignment-submission.entity.ts CHANGED
@@ -88,6 +88,36 @@ export class AssignmentSubmission {
88
  })
89
  submittedAt: Date;
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  @ManyToOne(() => Assignment, (assignment) => assignment.submissions, {
92
  onDelete: 'CASCADE',
93
  })
@@ -105,4 +135,11 @@ export class AssignmentSubmission {
105
  })
106
  @JoinColumn({ name: 'file_id' })
107
  file: File | null;
 
 
 
 
 
 
 
108
  }
 
88
  })
89
  submittedAt: Date;
90
 
91
+ @Column({
92
+ type: 'decimal',
93
+ precision: 5,
94
+ scale: 2,
95
+ nullable: true,
96
+ name: 'score',
97
+ })
98
+ score: number | null;
99
+
100
+ @Column({
101
+ type: 'text',
102
+ nullable: true,
103
+ name: 'feedback',
104
+ })
105
+ feedback: string | null;
106
+
107
+ @Column({
108
+ type: 'bigint',
109
+ nullable: true,
110
+ name: 'graded_by',
111
+ })
112
+ gradedBy: number | null;
113
+
114
+ @Column({
115
+ type: 'timestamp',
116
+ nullable: true,
117
+ name: 'graded_at',
118
+ })
119
+ gradedAt: Date | null;
120
+
121
  @ManyToOne(() => Assignment, (assignment) => assignment.submissions, {
122
  onDelete: 'CASCADE',
123
  })
 
135
  })
136
  @JoinColumn({ name: 'file_id' })
137
  file: File | null;
138
+
139
+ @ManyToOne(() => User, {
140
+ onDelete: 'SET NULL',
141
+ nullable: true,
142
+ })
143
+ @JoinColumn({ name: 'graded_by' })
144
+ grader: User | null;
145
  }
src/modules/assignments/services/assignments.service.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Injectable, Logger, BadRequestException } from '@nestjs/common';
2
  import { InjectRepository } from '@nestjs/typeorm';
3
  import { Repository } from 'typeorm';
4
  import { Assignment, AssignmentSubmission } from '../entities';
@@ -19,6 +19,8 @@ import { Course } from '../../courses/entities/course.entity';
19
  import { CourseEnrollment } from '../../enrollments/entities/course-enrollment.entity';
20
  import { DriveFolderService } from '../../google-drive/services/drive-folder.service';
21
  import { DriveFileEntityType } from '../../google-drive/entities/drive-file.entity';
 
 
22
 
23
  @Injectable()
24
  export class AssignmentsService {
@@ -34,6 +36,8 @@ export class AssignmentsService {
34
  @InjectRepository(CourseEnrollment)
35
  private enrollmentRepo: Repository<CourseEnrollment>,
36
  private driveFolderService: DriveFolderService,
 
 
37
  ) {}
38
 
39
  async create(dto: CreateAssignmentDto, userId: number): Promise<Assignment> {
@@ -278,7 +282,7 @@ export class AssignmentsService {
278
  submissionId: number,
279
  dto: GradeSubmissionDto,
280
  graderId: number,
281
- ): Promise<{ submissionId: number; score: number; maxScore: number; feedback?: string }> {
282
  const submission = await this.submissionRepo.findOne({
283
  where: { id: submissionId, assignmentId },
284
  });
@@ -291,19 +295,37 @@ export class AssignmentsService {
291
  throw new AssignmentNotFoundException();
292
  }
293
 
 
294
  submission.submissionStatus = SubmissionStatus.GRADED;
 
 
 
 
295
  await this.submissionRepo.save(submission);
296
 
297
  this.logger.log(
298
  `Submission ${submissionId} graded by user ${graderId}: ${dto.score}/${assignment.maxScore}`,
299
  );
300
 
301
- // TODO: Create grade record in grades table when GradesModule is wired
 
 
 
 
 
 
 
 
 
 
 
 
302
  return {
303
  submissionId: submission.id,
304
  score: dto.score,
305
  maxScore: Number(assignment.maxScore),
306
  feedback: dto.feedback,
 
307
  };
308
  }
309
 
 
1
+ import { Injectable, Logger, BadRequestException, Inject, forwardRef } from '@nestjs/common';
2
  import { InjectRepository } from '@nestjs/typeorm';
3
  import { Repository } from 'typeorm';
4
  import { Assignment, AssignmentSubmission } from '../entities';
 
19
  import { CourseEnrollment } from '../../enrollments/entities/course-enrollment.entity';
20
  import { DriveFolderService } from '../../google-drive/services/drive-folder.service';
21
  import { DriveFileEntityType } from '../../google-drive/entities/drive-file.entity';
22
+ import { GradesService } from '../../grades/services';
23
+ import { GradeType } from '../../grades/enums';
24
 
25
  @Injectable()
26
  export class AssignmentsService {
 
36
  @InjectRepository(CourseEnrollment)
37
  private enrollmentRepo: Repository<CourseEnrollment>,
38
  private driveFolderService: DriveFolderService,
39
+ @Inject(forwardRef(() => GradesService))
40
+ private gradesService: GradesService,
41
  ) {}
42
 
43
  async create(dto: CreateAssignmentDto, userId: number): Promise<Assignment> {
 
282
  submissionId: number,
283
  dto: GradeSubmissionDto,
284
  graderId: number,
285
+ ): Promise<{ submissionId: number; score: number; maxScore: number; feedback?: string; gradeId?: number }> {
286
  const submission = await this.submissionRepo.findOne({
287
  where: { id: submissionId, assignmentId },
288
  });
 
295
  throw new AssignmentNotFoundException();
296
  }
297
 
298
+ // Update submission with grading info
299
  submission.submissionStatus = SubmissionStatus.GRADED;
300
+ submission.score = dto.score;
301
+ submission.feedback = dto.feedback || null;
302
+ submission.gradedBy = graderId;
303
+ submission.gradedAt = new Date();
304
  await this.submissionRepo.save(submission);
305
 
306
  this.logger.log(
307
  `Submission ${submissionId} graded by user ${graderId}: ${dto.score}/${assignment.maxScore}`,
308
  );
309
 
310
+ // Create grade record in central grades table
311
+ const grade = await this.gradesService.createGrade({
312
+ userId: submission.userId,
313
+ courseId: assignment.courseId,
314
+ gradeType: GradeType.ASSIGNMENT,
315
+ assignmentId: assignmentId,
316
+ score: dto.score,
317
+ maxScore: Number(assignment.maxScore),
318
+ feedback: dto.feedback,
319
+ isPublished: true, // Assignment grades are immediately visible
320
+ }, graderId);
321
+ this.logger.log(`Grade record created: ${grade.id} for assignment ${assignmentId}, user ${submission.userId}`);
322
+
323
  return {
324
  submissionId: submission.id,
325
  score: dto.score,
326
  maxScore: Number(assignment.maxScore),
327
  feedback: dto.feedback,
328
+ gradeId: Number(grade.id),
329
  };
330
  }
331
 
src/modules/labs/controllers/labs.controller.ts CHANGED
@@ -43,6 +43,8 @@ import {
43
  UploadLabTaMaterialDto,
44
  UploadLabSubmissionDto,
45
  } from '../dto';
 
 
46
 
47
  @ApiTags('Labs')
48
  @ApiBearerAuth('JWT-auth')
@@ -118,6 +120,41 @@ export class LabsController {
118
  return this.labsService.remove(id);
119
  }
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  // ============ INSTRUCTIONS ============
122
 
123
  @Get(':id/instructions')
@@ -191,7 +228,7 @@ export class LabsController {
191
  @Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN)
192
  @ApiOperation({
193
  summary: 'Grade lab submission',
194
- description: 'Update the status of a lab submission (grade, return, request resubmit).',
195
  })
196
  @ApiParam({ name: 'id', description: 'Lab ID', example: 1 })
197
  @ApiParam({ name: 'subId', description: 'Submission ID', example: 1 })
@@ -202,8 +239,10 @@ export class LabsController {
202
  @Param('id', ParseIntPipe) id: number,
203
  @Param('subId', ParseIntPipe) subId: number,
204
  @Body() dto: GradeLabSubmissionDto,
 
205
  ) {
206
- return this.labsService.gradeSubmission(id, subId, dto);
 
207
  }
208
 
209
  // ============ ATTENDANCE ============
 
43
  UploadLabTaMaterialDto,
44
  UploadLabSubmissionDto,
45
  } from '../dto';
46
+ import { LabStatus } from '../enums';
47
+ import { Lab } from '../entities/lab.entity';
48
 
49
  @ApiTags('Labs')
50
  @ApiBearerAuth('JWT-auth')
 
120
  return this.labsService.remove(id);
121
  }
122
 
123
+ @Patch(':id/status')
124
+ @Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN)
125
+ @ApiOperation({
126
+ summary: 'Change lab status',
127
+ description: `
128
+ Change the status of a lab (publish, close, or archive).
129
+
130
+ ### Status Values
131
+ - \`draft\`: Not visible to students
132
+ - \`published\`: Visible to students, accepting submissions
133
+ - \`closed\`: No longer accepting submissions
134
+ - \`archived\`: Hidden from all views
135
+ `,
136
+ })
137
+ @ApiParam({ name: 'id', description: 'Lab ID', type: Number })
138
+ @ApiBody({
139
+ schema: {
140
+ properties: {
141
+ status: {
142
+ type: 'string',
143
+ enum: ['draft', 'published', 'closed', 'archived'],
144
+ example: 'published',
145
+ },
146
+ },
147
+ },
148
+ })
149
+ @ApiResponse({ status: 200, description: 'Lab status updated', type: Lab })
150
+ @ApiResponse({ status: 404, description: 'Lab not found' })
151
+ async changeStatus(
152
+ @Param('id', ParseIntPipe) id: number,
153
+ @Body('status') status: LabStatus,
154
+ ): Promise<Lab> {
155
+ return this.labsService.changeStatus(id, status);
156
+ }
157
+
158
  // ============ INSTRUCTIONS ============
159
 
160
  @Get(':id/instructions')
 
228
  @Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN)
229
  @ApiOperation({
230
  summary: 'Grade lab submission',
231
+ description: 'Grade a lab submission with score, feedback, and status. Creates a grade record in the gradebook.',
232
  })
233
  @ApiParam({ name: 'id', description: 'Lab ID', example: 1 })
234
  @ApiParam({ name: 'subId', description: 'Submission ID', example: 1 })
 
239
  @Param('id', ParseIntPipe) id: number,
240
  @Param('subId', ParseIntPipe) subId: number,
241
  @Body() dto: GradeLabSubmissionDto,
242
+ @Req() req: any,
243
  ) {
244
+ const graderId = req.user.userId || req.user.id;
245
+ return this.labsService.gradeSubmission(id, subId, dto, graderId);
246
  }
247
 
248
  // ============ ATTENDANCE ============
src/modules/labs/dto/index.ts CHANGED
@@ -69,6 +69,17 @@ export class GradeLabSubmissionDto {
69
  @ApiProperty({ example: 'graded', enum: ['submitted', 'graded', 'returned', 'resubmit'], description: 'Submission status' })
70
  @IsEnum(['submitted', 'graded', 'returned', 'resubmit'])
71
  status: string;
 
 
 
 
 
 
 
 
 
 
 
72
  }
73
 
74
  export class CreateInstructionDto {
 
69
  @ApiProperty({ example: 'graded', enum: ['submitted', 'graded', 'returned', 'resubmit'], description: 'Submission status' })
70
  @IsEnum(['submitted', 'graded', 'returned', 'resubmit'])
71
  status: string;
72
+
73
+ @ApiPropertyOptional({ example: 85, description: 'Score for the submission' })
74
+ @IsOptional()
75
+ @IsNumber()
76
+ @Min(0)
77
+ score?: number;
78
+
79
+ @ApiPropertyOptional({ example: 'Good work! Consider optimizing your code for better performance.', description: 'Feedback for the student' })
80
+ @IsOptional()
81
+ @IsString()
82
+ feedback?: string;
83
  }
84
 
85
  export class CreateInstructionDto {
src/modules/labs/entities/lab-submission.entity.ts CHANGED
@@ -35,6 +35,18 @@ export class LabSubmission {
35
  @Column({ name: 'status', type: 'enum', enum: ['submitted', 'graded', 'returned', 'resubmit'], default: 'submitted' })
36
  status: string;
37
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  @ManyToOne(() => Lab, (lab) => lab.submissions)
39
  @JoinColumn({ name: 'lab_id' })
40
  lab: Lab;
@@ -46,4 +58,8 @@ export class LabSubmission {
46
  @ManyToOne(() => File, { onDelete: 'SET NULL', nullable: true })
47
  @JoinColumn({ name: 'file_id' })
48
  file: File;
 
 
 
 
49
  }
 
35
  @Column({ name: 'status', type: 'enum', enum: ['submitted', 'graded', 'returned', 'resubmit'], default: 'submitted' })
36
  status: string;
37
 
38
+ @Column({ name: 'score', type: 'decimal', precision: 5, scale: 2, nullable: true })
39
+ score: number | null;
40
+
41
+ @Column({ name: 'feedback', type: 'text', nullable: true })
42
+ feedback: string | null;
43
+
44
+ @Column({ name: 'graded_by', type: 'bigint', unsigned: true, nullable: true })
45
+ gradedBy: number | null;
46
+
47
+ @Column({ name: 'graded_at', type: 'timestamp', nullable: true })
48
+ gradedAt: Date | null;
49
+
50
  @ManyToOne(() => Lab, (lab) => lab.submissions)
51
  @JoinColumn({ name: 'lab_id' })
52
  lab: Lab;
 
58
  @ManyToOne(() => File, { onDelete: 'SET NULL', nullable: true })
59
  @JoinColumn({ name: 'file_id' })
60
  file: File;
61
+
62
+ @ManyToOne(() => User, { onDelete: 'SET NULL', nullable: true })
63
+ @JoinColumn({ name: 'graded_by' })
64
+ grader: User | null;
65
  }
src/modules/labs/labs.module.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Module } from '@nestjs/common';
2
  import { TypeOrmModule } from '@nestjs/typeorm';
3
  import { Lab } from './entities/lab.entity';
4
  import { LabSubmission } from './entities/lab-submission.entity';
@@ -7,11 +7,13 @@ import { LabAttendance } from './entities/lab-attendance.entity';
7
  import { LabsService } from './services/labs.service';
8
  import { LabsController } from './controllers/labs.controller';
9
  import { GoogleDriveModule } from '../google-drive/google-drive.module';
 
10
 
11
  @Module({
12
  imports: [
13
  TypeOrmModule.forFeature([Lab, LabSubmission, LabInstruction, LabAttendance]),
14
  GoogleDriveModule,
 
15
  ],
16
  controllers: [LabsController],
17
  providers: [LabsService],
 
1
+ import { Module, forwardRef } from '@nestjs/common';
2
  import { TypeOrmModule } from '@nestjs/typeorm';
3
  import { Lab } from './entities/lab.entity';
4
  import { LabSubmission } from './entities/lab-submission.entity';
 
7
  import { LabsService } from './services/labs.service';
8
  import { LabsController } from './controllers/labs.controller';
9
  import { GoogleDriveModule } from '../google-drive/google-drive.module';
10
+ import { GradesModule } from '../grades/grades.module';
11
 
12
  @Module({
13
  imports: [
14
  TypeOrmModule.forFeature([Lab, LabSubmission, LabInstruction, LabAttendance]),
15
  GoogleDriveModule,
16
+ forwardRef(() => GradesModule),
17
  ],
18
  controllers: [LabsController],
19
  providers: [LabsService],
src/modules/labs/services/labs.service.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
2
  import { InjectRepository } from '@nestjs/typeorm';
3
  import { Repository } from 'typeorm';
4
  import { Lab } from '../entities/lab.entity';
@@ -16,6 +16,9 @@ import {
16
  } from '../dto';
17
  import { DriveFolderService } from '../../google-drive/services/drive-folder.service';
18
  import { DriveFileEntityType } from '../../google-drive/entities/drive-file.entity';
 
 
 
19
 
20
  @Injectable()
21
  export class LabsService {
@@ -31,6 +34,8 @@ export class LabsService {
31
  @InjectRepository(LabAttendance)
32
  private attendanceRepository: Repository<LabAttendance>,
33
  private driveFolderService: DriveFolderService,
 
 
34
  ) {}
35
 
36
  // ============ LABS CRUD ============
@@ -81,6 +86,14 @@ export class LabsService {
81
  this.logger.log(`Lab deleted: ${id}`);
82
  }
83
 
 
 
 
 
 
 
 
 
84
  // ============ INSTRUCTIONS ============
85
 
86
  async getInstructions(labId: number): Promise<LabInstruction[]> {
@@ -138,19 +151,45 @@ export class LabsService {
138
  });
139
  }
140
 
141
- async gradeSubmission(labId: number, submissionId: number, dto: GradeLabSubmissionDto): Promise<LabSubmission> {
142
- await this.findById(labId);
143
  const submission = await this.submissionRepository.findOne({
144
  where: { id: submissionId, labId },
145
  });
146
  if (!submission) throw new NotFoundException(`Submission ${submissionId} not found for lab ${labId}`);
147
 
 
148
  submission.status = dto.status;
 
 
 
 
 
 
 
 
 
149
  const updated = await this.submissionRepository.save(submission);
150
- this.logger.log(`Submission ${submissionId} graded: ${dto.status}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  return this.submissionRepository.findOne({
152
  where: { id: updated.id },
153
- relations: ['user', 'file'],
154
  }) as Promise<LabSubmission>;
155
  }
156
 
 
1
+ import { Injectable, Logger, NotFoundException, BadRequestException, Inject, forwardRef } from '@nestjs/common';
2
  import { InjectRepository } from '@nestjs/typeorm';
3
  import { Repository } from 'typeorm';
4
  import { Lab } from '../entities/lab.entity';
 
16
  } from '../dto';
17
  import { DriveFolderService } from '../../google-drive/services/drive-folder.service';
18
  import { DriveFileEntityType } from '../../google-drive/entities/drive-file.entity';
19
+ import { GradesService } from '../../grades/services';
20
+ import { GradeType } from '../../grades/enums';
21
+ import { LabStatus } from '../enums';
22
 
23
  @Injectable()
24
  export class LabsService {
 
34
  @InjectRepository(LabAttendance)
35
  private attendanceRepository: Repository<LabAttendance>,
36
  private driveFolderService: DriveFolderService,
37
+ @Inject(forwardRef(() => GradesService))
38
+ private gradesService: GradesService,
39
  ) {}
40
 
41
  // ============ LABS CRUD ============
 
86
  this.logger.log(`Lab deleted: ${id}`);
87
  }
88
 
89
+ async changeStatus(id: number, status: LabStatus): Promise<Lab> {
90
+ const lab = await this.findById(id);
91
+ lab.status = status;
92
+ const updated = await this.labRepository.save(lab);
93
+ this.logger.log(`Lab status changed to ${status}: ${id}`);
94
+ return updated;
95
+ }
96
+
97
  // ============ INSTRUCTIONS ============
98
 
99
  async getInstructions(labId: number): Promise<LabInstruction[]> {
 
151
  });
152
  }
153
 
154
+ async gradeSubmission(labId: number, submissionId: number, dto: GradeLabSubmissionDto, graderId: number): Promise<LabSubmission> {
155
+ const lab = await this.findById(labId);
156
  const submission = await this.submissionRepository.findOne({
157
  where: { id: submissionId, labId },
158
  });
159
  if (!submission) throw new NotFoundException(`Submission ${submissionId} not found for lab ${labId}`);
160
 
161
+ // Update submission with grading info
162
  submission.status = dto.status;
163
+ if (dto.score !== undefined) {
164
+ submission.score = dto.score;
165
+ }
166
+ if (dto.feedback !== undefined) {
167
+ submission.feedback = dto.feedback;
168
+ }
169
+ submission.gradedBy = graderId;
170
+ submission.gradedAt = new Date();
171
+
172
  const updated = await this.submissionRepository.save(submission);
173
+ this.logger.log(`Submission ${submissionId} graded: ${dto.status}, score: ${dto.score}`);
174
+
175
+ // Create or update grade record in central grades table
176
+ if (dto.score !== undefined && dto.status === 'graded') {
177
+ await this.gradesService.createGrade({
178
+ userId: submission.userId,
179
+ courseId: lab.courseId,
180
+ gradeType: GradeType.LAB,
181
+ labId: labId,
182
+ score: dto.score,
183
+ maxScore: Number(lab.maxScore),
184
+ feedback: dto.feedback,
185
+ isPublished: true, // Lab grades are immediately visible
186
+ }, graderId);
187
+ this.logger.log(`Grade record created for lab ${labId}, user ${submission.userId}`);
188
+ }
189
+
190
  return this.submissionRepository.findOne({
191
  where: { id: updated.id },
192
+ relations: ['user', 'file', 'grader'],
193
  }) as Promise<LabSubmission>;
194
  }
195
 
src/modules/quizzes/controllers/quizzes.controller.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
  Post,
5
  Put,
6
  Delete,
 
7
  Body,
8
  Param,
9
  Query,
@@ -43,6 +44,7 @@ import {
43
  ReorderQuestionsDto,
44
  } from '../dto';
45
  import { Quiz, QuizQuestion, QuizAttempt, QuizDifficultyLevel } from '../entities';
 
46
 
47
  @ApiTags('📝 Quizzes')
48
  @ApiBearerAuth('JWT-auth')
@@ -392,6 +394,58 @@ Soft deletes a quiz. The quiz data is preserved but hidden from users.
392
  return { message: 'Quiz deleted successfully' };
393
  }
394
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  // ============ QUESTION MANAGEMENT ============
396
 
397
  @Post(':quizId/questions')
@@ -774,8 +828,10 @@ Apply manual grades to essay and short answer questions that require human evalu
774
  async applyManualGrades(
775
  @Param('attemptId', ParseIntPipe) attemptId: number,
776
  @Body() dto: ManualGradeDto,
 
777
  ): Promise<AttemptResultDto> {
778
- return this.gradingService.applyManualGrades(attemptId, dto);
 
779
  }
780
 
781
  @Get('attempts/:attemptId/pending-grading')
 
4
  Post,
5
  Put,
6
  Delete,
7
+ Patch,
8
  Body,
9
  Param,
10
  Query,
 
44
  ReorderQuestionsDto,
45
  } from '../dto';
46
  import { Quiz, QuizQuestion, QuizAttempt, QuizDifficultyLevel } from '../entities';
47
+ import { QuizStatus } from '../enums';
48
 
49
  @ApiTags('📝 Quizzes')
50
  @ApiBearerAuth('JWT-auth')
 
394
  return { message: 'Quiz deleted successfully' };
395
  }
396
 
397
+ @Patch(':id/status')
398
+ @Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN)
399
+ @ApiOperation({
400
+ summary: 'Change quiz status',
401
+ description: `
402
+ ## Change Quiz Status
403
+
404
+ Updates the status of a quiz (publish, close, or archive).
405
+
406
+ ### Access Control
407
+ - **Authentication Required**: ✅ Yes (Bearer Token)
408
+ - **Roles Required**: INSTRUCTOR, TA, ADMIN
409
+
410
+ ### Status Values
411
+ - \`draft\`: Not visible to students
412
+ - \`published\`: Available for students to take
413
+ - \`closed\`: No longer accepting attempts
414
+ - \`archived\`: Hidden from all views
415
+
416
+ ### Behavior
417
+ - Students can only see published quizzes
418
+ - Closed quizzes preserve existing attempts but prevent new ones
419
+ - Archived quizzes are soft-hidden
420
+ `,
421
+ })
422
+ @ApiParam({ name: 'id', description: 'Quiz ID', type: Number })
423
+ @ApiBody({
424
+ schema: {
425
+ properties: {
426
+ status: {
427
+ type: 'string',
428
+ enum: ['draft', 'published', 'closed', 'archived'],
429
+ example: 'published',
430
+ },
431
+ },
432
+ },
433
+ })
434
+ @ApiResponse({ status: 200, description: 'Quiz status updated', type: Quiz })
435
+ @ApiResponse({ status: 404, description: 'Quiz not found' })
436
+ async changeStatus(
437
+ @Param('id', ParseIntPipe) id: number,
438
+ @Body('status') status: QuizStatus,
439
+ @Req() req: any,
440
+ ): Promise<Quiz> {
441
+ return this.quizzesService.changeStatus(
442
+ id,
443
+ status,
444
+ req.user.userId,
445
+ this.getRoleNames(req.user),
446
+ );
447
+ }
448
+
449
  // ============ QUESTION MANAGEMENT ============
450
 
451
  @Post(':quizId/questions')
 
828
  async applyManualGrades(
829
  @Param('attemptId', ParseIntPipe) attemptId: number,
830
  @Body() dto: ManualGradeDto,
831
+ @Req() req: any,
832
  ): Promise<AttemptResultDto> {
833
+ const graderId = req.user.userId;
834
+ return this.gradingService.applyManualGrades(attemptId, dto, graderId);
835
  }
836
 
837
  @Get('attempts/:attemptId/pending-grading')
src/modules/quizzes/entities/quiz.entity.ts CHANGED
@@ -11,7 +11,7 @@ import {
11
  } from 'typeorm';
12
  import { Course } from '../../courses/entities/course.entity';
13
  import { User } from '../../auth/entities/user.entity';
14
- import { QuizType, ShowAnswersAfter } from '../enums';
15
  import { QuizQuestion } from './quiz-question.entity';
16
  import { QuizAttempt } from './quiz-attempt.entity';
17
 
@@ -40,6 +40,14 @@ export class Quiz {
40
  })
41
  quizType: QuizType;
42
 
 
 
 
 
 
 
 
 
43
  @Column({ name: 'time_limit_minutes', type: 'int', nullable: true })
44
  timeLimitMinutes: number;
45
 
 
11
  } from 'typeorm';
12
  import { Course } from '../../courses/entities/course.entity';
13
  import { User } from '../../auth/entities/user.entity';
14
+ import { QuizType, QuizStatus, ShowAnswersAfter } from '../enums';
15
  import { QuizQuestion } from './quiz-question.entity';
16
  import { QuizAttempt } from './quiz-attempt.entity';
17
 
 
40
  })
41
  quizType: QuizType;
42
 
43
+ @Column({
44
+ name: 'status',
45
+ type: 'enum',
46
+ enum: QuizStatus,
47
+ default: QuizStatus.DRAFT,
48
+ })
49
+ status: QuizStatus;
50
+
51
  @Column({ name: 'time_limit_minutes', type: 'int', nullable: true })
52
  timeLimitMinutes: number;
53
 
src/modules/quizzes/enums/index.ts CHANGED
@@ -1,4 +1,5 @@
1
  export * from './question-type.enum';
2
  export * from './quiz-type.enum';
 
3
  export * from './attempt-status.enum';
4
  export * from './show-answers.enum';
 
1
  export * from './question-type.enum';
2
  export * from './quiz-type.enum';
3
+ export * from './quiz-status.enum';
4
  export * from './attempt-status.enum';
5
  export * from './show-answers.enum';
src/modules/quizzes/enums/quiz-status.enum.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export enum QuizStatus {
2
+ DRAFT = 'draft',
3
+ PUBLISHED = 'published',
4
+ CLOSED = 'closed',
5
+ ARCHIVED = 'archived',
6
+ }
src/modules/quizzes/quizzes.module.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Module } from '@nestjs/common';
2
  import { TypeOrmModule } from '@nestjs/typeorm';
3
  import { QuizzesController } from './controllers/quizzes.controller';
4
  import { QuizzesService, QuizGradingService } from './services';
@@ -6,6 +6,7 @@ import { Quiz, QuizQuestion, QuizAttempt, QuizAnswer, QuizDifficultyLevel } from
6
  import { Course } from '../courses/entities/course.entity';
7
  import { CourseTA } from '../enrollments/entities/course-ta.entity';
8
  import { CourseInstructor } from '../enrollments/entities/course-instructor.entity';
 
9
 
10
  @Module({
11
  imports: [
@@ -19,6 +20,7 @@ import { CourseInstructor } from '../enrollments/entities/course-instructor.enti
19
  CourseTA,
20
  CourseInstructor,
21
  ]),
 
22
  ],
23
  controllers: [QuizzesController],
24
  providers: [QuizzesService, QuizGradingService],
 
1
+ import { Module, forwardRef } from '@nestjs/common';
2
  import { TypeOrmModule } from '@nestjs/typeorm';
3
  import { QuizzesController } from './controllers/quizzes.controller';
4
  import { QuizzesService, QuizGradingService } from './services';
 
6
  import { Course } from '../courses/entities/course.entity';
7
  import { CourseTA } from '../enrollments/entities/course-ta.entity';
8
  import { CourseInstructor } from '../enrollments/entities/course-instructor.entity';
9
+ import { GradesModule } from '../grades/grades.module';
10
 
11
  @Module({
12
  imports: [
 
20
  CourseTA,
21
  CourseInstructor,
22
  ]),
23
+ forwardRef(() => GradesModule),
24
  ],
25
  controllers: [QuizzesController],
26
  providers: [QuizzesService, QuizGradingService],
src/modules/quizzes/services/quiz-grading.service.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { Injectable } from '@nestjs/common';
2
  import { InjectRepository } from '@nestjs/typeorm';
3
  import { Repository } from 'typeorm';
4
- import { QuizAttempt, QuizAnswer, QuizQuestion } from '../entities';
5
  import { QuestionType, AttemptStatus } from '../enums';
6
  import { ManualGradeDto, AttemptResultDto } from '../dto';
7
  import { AttemptNotFoundException } from '../exceptions';
 
 
8
 
9
  @Injectable()
10
  export class QuizGradingService {
@@ -15,12 +17,14 @@ export class QuizGradingService {
15
  private readonly answerRepo: Repository<QuizAnswer>,
16
  @InjectRepository(QuizQuestion)
17
  private readonly questionRepo: Repository<QuizQuestion>,
 
 
18
  ) {}
19
 
20
  /**
21
  * Apply manual grades to essay/short answer questions
22
  */
23
- async applyManualGrades(attemptId: number, dto: ManualGradeDto): Promise<AttemptResultDto> {
24
  const attempt = await this.attemptRepo.findOne({
25
  where: { id: attemptId },
26
  relations: ['quiz', 'quiz.questions', 'answers', 'user'],
@@ -41,6 +45,7 @@ export class QuizGradingService {
41
  // Recalculate total score
42
  const answers = await this.answerRepo.find({ where: { attemptId } });
43
  const totalScore = answers.reduce((sum, a) => sum + Number(a.pointsEarned || 0), 0);
 
44
 
45
  // Update attempt status to graded
46
  await this.attemptRepo.update(attemptId, {
@@ -48,6 +53,17 @@ export class QuizGradingService {
48
  status: AttemptStatus.GRADED,
49
  });
50
 
 
 
 
 
 
 
 
 
 
 
 
51
  return this.buildAttemptResult(attemptId);
52
  }
53
 
 
1
+ import { Injectable, Inject, forwardRef } from '@nestjs/common';
2
  import { InjectRepository } from '@nestjs/typeorm';
3
  import { Repository } from 'typeorm';
4
+ import { QuizAttempt, QuizAnswer, QuizQuestion, Quiz } from '../entities';
5
  import { QuestionType, AttemptStatus } from '../enums';
6
  import { ManualGradeDto, AttemptResultDto } from '../dto';
7
  import { AttemptNotFoundException } from '../exceptions';
8
+ import { GradesService } from '../../grades/services';
9
+ import { GradeType } from '../../grades/enums';
10
 
11
  @Injectable()
12
  export class QuizGradingService {
 
17
  private readonly answerRepo: Repository<QuizAnswer>,
18
  @InjectRepository(QuizQuestion)
19
  private readonly questionRepo: Repository<QuizQuestion>,
20
+ @Inject(forwardRef(() => GradesService))
21
+ private readonly gradesService: GradesService,
22
  ) {}
23
 
24
  /**
25
  * Apply manual grades to essay/short answer questions
26
  */
27
+ async applyManualGrades(attemptId: number, dto: ManualGradeDto, graderId?: number): Promise<AttemptResultDto> {
28
  const attempt = await this.attemptRepo.findOne({
29
  where: { id: attemptId },
30
  relations: ['quiz', 'quiz.questions', 'answers', 'user'],
 
45
  // Recalculate total score
46
  const answers = await this.answerRepo.find({ where: { attemptId } });
47
  const totalScore = answers.reduce((sum, a) => sum + Number(a.pointsEarned || 0), 0);
48
+ const maxScore = attempt.quiz.questions.reduce((sum, q) => sum + Number(q.points), 0);
49
 
50
  // Update attempt status to graded
51
  await this.attemptRepo.update(attemptId, {
 
53
  status: AttemptStatus.GRADED,
54
  });
55
 
56
+ // Create/update grade record in central grades table
57
+ await this.gradesService.createGrade({
58
+ userId: attempt.userId,
59
+ courseId: attempt.quiz.courseId,
60
+ gradeType: GradeType.QUIZ,
61
+ quizId: attempt.quiz.id,
62
+ score: totalScore,
63
+ maxScore: maxScore,
64
+ isPublished: true,
65
+ }, graderId || attempt.quiz.createdBy);
66
+
67
  return this.buildAttemptResult(attemptId);
68
  }
69
 
src/modules/quizzes/services/quizzes.service.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { Injectable, BadRequestException, ForbiddenException } from '@nestjs/common';
2
  import { InjectRepository } from '@nestjs/typeorm';
3
  import { Repository, Between, Like, IsNull } from 'typeorm';
4
  import { Quiz, QuizQuestion, QuizAttempt, QuizAnswer, QuizDifficultyLevel } from '../entities';
5
- import { AttemptStatus, QuestionType } from '../enums';
6
  import {
7
  CreateQuizDto,
8
  UpdateQuizDto,
@@ -30,6 +30,8 @@ import { Course } from '../../courses/entities/course.entity';
30
  import { RoleName } from '../../auth/entities/role.entity';
31
  import { CourseTA } from '../../enrollments/entities/course-ta.entity';
32
  import { CourseInstructor } from '../../enrollments/entities/course-instructor.entity';
 
 
33
 
34
  @Injectable()
35
  export class QuizzesService {
@@ -50,6 +52,8 @@ export class QuizzesService {
50
  private readonly courseTARepo: Repository<CourseTA>,
51
  @InjectRepository(CourseInstructor)
52
  private readonly courseInstructorRepo: Repository<CourseInstructor>,
 
 
53
  ) {}
54
 
55
  // ============ QUIZ CRUD ============
@@ -156,6 +160,14 @@ export class QuizzesService {
156
  await this.quizRepo.softDelete(id);
157
  }
158
 
 
 
 
 
 
 
 
 
159
  // ============ QUESTION MANAGEMENT ============
160
 
161
  async addQuestion(
@@ -368,13 +380,28 @@ export class QuizzesService {
368
  (q) => q.questionType === QuestionType.SHORT_ANSWER || q.questionType === QuestionType.ESSAY,
369
  );
370
 
 
 
371
  await this.attemptRepo.update(attemptId, {
372
  submittedAt: new Date(),
373
  score: totalScore,
374
  timeTakenMinutes: timeTaken,
375
- status: needsManualGrading ? AttemptStatus.SUBMITTED : AttemptStatus.GRADED,
376
  });
377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  return this.getAttemptResult(attemptId, userId);
379
  }
380
 
 
1
+ import { Injectable, BadRequestException, ForbiddenException, Inject, forwardRef } from '@nestjs/common';
2
  import { InjectRepository } from '@nestjs/typeorm';
3
  import { Repository, Between, Like, IsNull } from 'typeorm';
4
  import { Quiz, QuizQuestion, QuizAttempt, QuizAnswer, QuizDifficultyLevel } from '../entities';
5
+ import { AttemptStatus, QuestionType, QuizStatus } from '../enums';
6
  import {
7
  CreateQuizDto,
8
  UpdateQuizDto,
 
30
  import { RoleName } from '../../auth/entities/role.entity';
31
  import { CourseTA } from '../../enrollments/entities/course-ta.entity';
32
  import { CourseInstructor } from '../../enrollments/entities/course-instructor.entity';
33
+ import { GradesService } from '../../grades/services';
34
+ import { GradeType } from '../../grades/enums';
35
 
36
  @Injectable()
37
  export class QuizzesService {
 
52
  private readonly courseTARepo: Repository<CourseTA>,
53
  @InjectRepository(CourseInstructor)
54
  private readonly courseInstructorRepo: Repository<CourseInstructor>,
55
+ @Inject(forwardRef(() => GradesService))
56
+ private readonly gradesService: GradesService,
57
  ) {}
58
 
59
  // ============ QUIZ CRUD ============
 
160
  await this.quizRepo.softDelete(id);
161
  }
162
 
163
+ async changeStatus(id: number, status: QuizStatus, userId: number, roles: string[]): Promise<Quiz> {
164
+ const quiz = await this.findQuizById(id);
165
+ await this.assertQuizManagementAccess(quiz, userId, roles);
166
+
167
+ quiz.status = status;
168
+ return this.quizRepo.save(quiz);
169
+ }
170
+
171
  // ============ QUESTION MANAGEMENT ============
172
 
173
  async addQuestion(
 
380
  (q) => q.questionType === QuestionType.SHORT_ANSWER || q.questionType === QuestionType.ESSAY,
381
  );
382
 
383
+ const finalStatus = needsManualGrading ? AttemptStatus.SUBMITTED : AttemptStatus.GRADED;
384
+
385
  await this.attemptRepo.update(attemptId, {
386
  submittedAt: new Date(),
387
  score: totalScore,
388
  timeTakenMinutes: timeTaken,
389
+ status: finalStatus,
390
  });
391
 
392
+ // Create grade record in central grades table if fully graded (no manual grading needed)
393
+ if (!needsManualGrading) {
394
+ await this.gradesService.createGrade({
395
+ userId: userId,
396
+ courseId: attempt.quiz.courseId,
397
+ gradeType: GradeType.QUIZ,
398
+ quizId: attempt.quiz.id,
399
+ score: totalScore,
400
+ maxScore: maxScore,
401
+ isPublished: true,
402
+ }, attempt.quiz.createdBy);
403
+ }
404
+
405
  return this.getAttemptResult(attemptId, userId);
406
  }
407