Spaces:
Sleeping
Sleeping
Labs - Quizzes - Assignment student Dashboard still under work
Browse files- COMPLETE_API_ENDPOINTS.md +0 -317
- COMPLETE_MODULE_ANALYSIS.md +0 -954
- DATABASE_SCHEMA.md +0 -508
- DB_CHANGES_GRADING_FIX.sql +113 -0
- DB_SCHEMA_FIXES.sql +0 -64
- Documentation/ASSESSMENT_INTEGRATION_GUIDE.md +846 -0
- Documentation/ASSIGNMENTS_FRONTEND_GUIDE.md +508 -0
- Documentation/LABS_FRONTEND_GUIDE.md +661 -0
- Documentation/QUIZZES_FRONTEND_GUIDE.md +508 -0
- EduVerse API - Complete Test Collection Copy.postman_test_run1 +0 -0
- EduVerse API - Complete Test Collection.postman_test_run.json +0 -0
- EduVerse_Postman_Collection.json +71 -0
- howtotest.md +910 -0
- integrationPlan.md +445 -0
- src/modules/assignments/assignments.module.ts +2 -0
- src/modules/assignments/entities/assignment-submission.entity.ts +37 -0
- src/modules/assignments/services/assignments.service.ts +25 -3
- src/modules/labs/controllers/labs.controller.ts +41 -2
- src/modules/labs/dto/index.ts +11 -0
- src/modules/labs/entities/lab-submission.entity.ts +16 -0
- src/modules/labs/labs.module.ts +3 -1
- src/modules/labs/services/labs.service.ts +44 -5
- src/modules/quizzes/controllers/quizzes.controller.ts +57 -1
- src/modules/quizzes/entities/quiz.entity.ts +9 -1
- src/modules/quizzes/enums/index.ts +1 -0
- src/modules/quizzes/enums/quiz-status.enum.ts +6 -0
- src/modules/quizzes/quizzes.module.ts +3 -1
- src/modules/quizzes/services/quiz-grading.service.ts +19 -3
- src/modules/quizzes/services/quizzes.service.ts +30 -3
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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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: '
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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:
|
| 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 |
|