@citolab/qti-backend-firebase 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/api/app-specific/base/application-specific-base.d.ts +11 -20
  2. package/dist/api/app-specific/base/application-specific-base.d.ts.map +1 -1
  3. package/dist/api/app-specific/base/application-specific-base.js +73 -88
  4. package/dist/api/app-specific/base/application-specific-base.js.map +1 -1
  5. package/dist/api/app-specific/baseImplementation.d.ts +2 -3
  6. package/dist/api/app-specific/baseImplementation.d.ts.map +1 -1
  7. package/dist/api/app-specific/baseImplementation.js +2 -6
  8. package/dist/api/app-specific/baseImplementation.js.map +1 -1
  9. package/dist/api/app-specific/index.d.ts +1 -0
  10. package/dist/api/app-specific/index.d.ts.map +1 -1
  11. package/dist/api/app-specific/index.js +1 -0
  12. package/dist/api/app-specific/index.js.map +1 -1
  13. package/dist/api/app-specific/interface/IApplicationSpecific.d.ts +10 -20
  14. package/dist/api/app-specific/interface/IApplicationSpecific.d.ts.map +1 -1
  15. package/dist/api/qti-data.d.ts.map +1 -1
  16. package/dist/api/qti-data.js +619 -271
  17. package/dist/api/qti-data.js.map +1 -1
  18. package/dist/api/qti-resources.d.ts.map +1 -1
  19. package/dist/api/qti-resources.js +134 -131
  20. package/dist/api/qti-resources.js.map +1 -1
  21. package/dist/api/qti-teacher.d.ts.map +1 -1
  22. package/dist/api/qti-teacher.js +563 -424
  23. package/dist/api/qti-teacher.js.map +1 -1
  24. package/dist/api/qti-tools.d.ts.map +1 -1
  25. package/dist/api/qti-tools.js +213 -41
  26. package/dist/api/qti-tools.js.map +1 -1
  27. package/dist/helpers/database.d.ts +35 -24
  28. package/dist/helpers/database.d.ts.map +1 -1
  29. package/dist/helpers/database.js +47 -36
  30. package/dist/helpers/database.js.map +1 -1
  31. package/dist/helpers/db.d.ts +1 -1
  32. package/dist/helpers/db.d.ts.map +1 -1
  33. package/dist/helpers/db.js +3 -7
  34. package/dist/helpers/db.js.map +1 -1
  35. package/dist/helpers/endpoint-helpers.d.ts +2 -2
  36. package/dist/helpers/endpoint-helpers.d.ts.map +1 -1
  37. package/dist/helpers/endpoint-helpers.js +59 -17
  38. package/dist/helpers/endpoint-helpers.js.map +1 -1
  39. package/dist/helpers/excel-helper.d.ts +29 -0
  40. package/dist/helpers/excel-helper.d.ts.map +1 -0
  41. package/dist/helpers/excel-helper.js +150 -0
  42. package/dist/helpers/excel-helper.js.map +1 -0
  43. package/dist/helpers/general.d.ts +15 -0
  44. package/dist/helpers/general.d.ts.map +1 -0
  45. package/dist/helpers/general.js +148 -0
  46. package/dist/helpers/general.js.map +1 -0
  47. package/dist/helpers/index.d.ts +2 -0
  48. package/dist/helpers/index.d.ts.map +1 -1
  49. package/dist/helpers/index.js +2 -0
  50. package/dist/helpers/index.js.map +1 -1
  51. package/dist/helpers/logic.d.ts +100 -41
  52. package/dist/helpers/logic.d.ts.map +1 -1
  53. package/dist/helpers/logic.js +523 -413
  54. package/dist/helpers/logic.js.map +1 -1
  55. package/dist/helpers/package-upload.d.ts.map +1 -1
  56. package/dist/helpers/package-upload.js +2 -3
  57. package/dist/helpers/package-upload.js.map +1 -1
  58. package/dist/helpers/package.d.ts +3 -4
  59. package/dist/helpers/package.d.ts.map +1 -1
  60. package/dist/helpers/package.js +12 -17
  61. package/dist/helpers/package.js.map +1 -1
  62. package/dist/helpers/request-headers.d.ts +4 -0
  63. package/dist/helpers/request-headers.d.ts.map +1 -1
  64. package/dist/helpers/request-headers.js +2 -2
  65. package/dist/helpers/request-headers.js.map +1 -1
  66. package/dist/helpers/resource-cache.d.ts +35 -0
  67. package/dist/helpers/resource-cache.d.ts.map +1 -0
  68. package/dist/helpers/resource-cache.js +171 -0
  69. package/dist/helpers/resource-cache.js.map +1 -0
  70. package/dist/helpers/storage.d.ts +2 -2
  71. package/dist/helpers/storage.d.ts.map +1 -1
  72. package/dist/helpers/storage.js +16 -15
  73. package/dist/helpers/storage.js.map +1 -1
  74. package/dist/helpers/utils.d.ts +4 -3
  75. package/dist/helpers/utils.d.ts.map +1 -1
  76. package/dist/helpers/utils.js +73 -51
  77. package/dist/helpers/utils.js.map +1 -1
  78. package/package.json +12 -9
@@ -1,11 +1,13 @@
1
- import { authenticateTeacher, getAppId } from "../helpers/request-headers";
2
- import { createSession, createSessionByIdentifications, deleteStudent, getApplication, getItemsStats, getPlannedStudents, resetSession, updateItemStatResponseScore, updateStudent, updateStudentSessionState, } from "./../helpers/logic";
1
+ import { Router } from "express";
2
+ import { authenticateTeacher } from "../helpers/request-headers";
3
+ import { createDelivery, deleteDelivery, getAssessment, getAssessments, reopenSession, resetSession, processFeedbackSubmission, validateFeedbackData, saveFeedback, } from "./../helpers/logic";
3
4
  import { getDatabase } from "../helpers/db";
4
5
  import { getFirestore } from "../helpers/firebase";
5
- import { body, param, query } from "express-validator";
6
+ import { body, param } from "express-validator";
6
7
  import { generalRateLimit, handleValidationErrors, heavyOperationLimit, sendError, sendSuccess, validateAssessmentId, } from "../helpers/endpoint-helpers";
7
- import { dateId, createCode } from "../helpers";
8
8
  import { BaseImplementation } from "./app-specific/baseImplementation";
9
+ import { DeliveryStateEnum } from "@citolab/qti-api";
10
+ import { sort } from "../helpers";
9
11
  // Teacher authentication middleware
10
12
  export const requireTeacherAuth = async (req, res, next) => {
11
13
  try {
@@ -14,7 +16,6 @@ export const requireTeacherAuth = async (req, res, next) => {
14
16
  return sendError(res, 401, "Teacher authentication required");
15
17
  }
16
18
  req.user = result.user;
17
- req.appId = getAppId(req);
18
19
  next();
19
20
  }
20
21
  catch (error) {
@@ -22,32 +23,6 @@ export const requireTeacherAuth = async (req, res, next) => {
22
23
  return sendError(res, 500, "Authentication failed");
23
24
  }
24
25
  };
25
- const checkCode = async (db, groupCode) => {
26
- try {
27
- const firestore = getFirestore();
28
- const doc = await firestore.doc(db.GROUP_DELIVERY.DOC(groupCode)).get();
29
- return doc.exists;
30
- }
31
- catch (error) {
32
- console.error("Error checking code:", error);
33
- return false;
34
- }
35
- };
36
- const createDelivery = async (db) => {
37
- try {
38
- let groupCode = createCode(4);
39
- let exists = await checkCode(db, groupCode);
40
- while (exists || groupCode.toLowerCase() === "aaaa") {
41
- groupCode = createCode(4);
42
- exists = await checkCode(db, groupCode);
43
- }
44
- return groupCode;
45
- }
46
- catch (error) {
47
- console.error("Error creating activity:", error);
48
- return null;
49
- }
50
- };
51
26
  // Validation helpers
52
27
  const validateCode = (code) => {
53
28
  return (typeof code === "string" &&
@@ -55,32 +30,11 @@ const validateCode = (code) => {
55
30
  code.length <= 50 &&
56
31
  /^[a-zA-Z0-9_-]+$/.test(code));
57
32
  };
58
- const validateTarget = (target) => {
59
- return target === "teacher" || target === "reviewer";
60
- };
61
- const parseScore = (scoreValue) => {
62
- if (scoreValue === null || scoreValue === undefined || scoreValue === "") {
63
- return null;
64
- }
65
- if (typeof scoreValue === "number") {
66
- return isFinite(scoreValue) ? scoreValue : null;
67
- }
68
- if (typeof scoreValue === "string") {
69
- // Valid integer (positive or negative)
70
- if (/^-?\d+$/.test(scoreValue)) {
71
- return parseInt(scoreValue, 10);
72
- }
73
- // Valid float
74
- if (/^-?\d*\.\d+$/.test(scoreValue)) {
75
- return parseFloat(scoreValue);
76
- }
77
- }
78
- return null;
79
- };
80
33
  export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImplementation()) {
34
+ const teacherRouter = Router();
81
35
  /**
82
36
  * @openapi
83
- * /teacher/startGroupDelivery:
37
+ * /delivery/create:
84
38
  * post:
85
39
  * tags:
86
40
  * - Teacher
@@ -117,124 +71,128 @@ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImp
117
71
  * 500:
118
72
  * description: Internal server error
119
73
  */
120
- app.post("/teacher/startGroupDelivery", [
74
+ teacherRouter.post("/delivery/create", [
121
75
  heavyOperationLimit,
122
76
  body("assessmentId").notEmpty().withMessage("Assessment ID is required"),
123
77
  handleValidationErrors,
124
78
  requireTeacherAuth,
125
79
  ], async (req, res) => {
126
80
  try {
81
+ const user = await authenticateTeacher(req, res);
82
+ const userId = user.user?.uid || "";
127
83
  const { assessmentId } = req.body;
128
- const db = getDatabase(req.appId);
129
- const code = await createDelivery(db);
84
+ const db = getDatabase();
85
+ const code = await createDelivery(db, userId, assessmentId);
130
86
  if (!code) {
131
87
  return sendError(res, 500, "Failed to create activity code");
132
88
  }
133
89
  const firestore = getFirestore();
134
- // Create the activity document
135
- await firestore.doc(db.GROUP_DELIVERY.DOC(code)).set({
136
- groupCode: code,
90
+ const delivery = {
137
91
  assessmentId,
138
- created: new Date().toISOString(),
139
- teacherId: req.user.uid,
140
- appId: req.appId,
141
- });
142
- return sendSuccess(res, { groupCode: code, assessmentId }, "Group delivery created successfully");
92
+ createdAt: new Date().getTime(),
93
+ updatedAt: new Date().getTime(),
94
+ createdBy: userId,
95
+ id: code,
96
+ state: DeliveryStateEnum.NOT_STARTED,
97
+ };
98
+ // Create the activity document
99
+ await firestore.doc(db.DELIVERY.DOC(code)).set(delivery);
100
+ return sendSuccess(res, delivery, "Group delivery created successfully");
143
101
  }
144
102
  catch (error) {
145
103
  console.error("Error creating group delivery:", error);
146
104
  return sendError(res, 500, "Failed to create group delivery");
147
105
  }
148
106
  });
107
+ // delete delivery
149
108
  /**
150
109
  * @openapi
151
- * /teacher/assessments:
152
- * get:
110
+ * /delivery/delete:
111
+ * post:
153
112
  * tags:
154
113
  * - Teacher
155
- * summary: Get all assessments
114
+ * summary: Delete a delivery
115
+ * requestBody:
116
+ * required: true
117
+ * content:
118
+ * application/json:
119
+ * schema:
120
+ * type: object
121
+ * required:
122
+ * - code
123
+ * properties:
124
+ * code:
125
+ * type: string
126
+ * description: The delivery code to delete
156
127
  * responses:
157
128
  * 200:
158
- * description: Returns all assessments
129
+ * description: Delivery deleted successfully
130
+ * 400:
131
+ * description: Invalid request data
159
132
  * 401:
160
133
  * description: Unauthorized
161
134
  * 404:
162
- * description: Application not found
135
+ * description: Delivery not found
163
136
  * 500:
164
137
  * description: Internal server error
165
138
  */
166
- app.get("/teacher/assessments", [generalRateLimit, requireTeacherAuth], async (req, res) => {
167
- try {
168
- const db = getDatabase(req.appId);
169
- const application = await getApplication(db);
170
- if (!application) {
171
- return sendError(res, 404, "Application not found");
139
+ teacherRouter.post("/delivery/delete", [
140
+ generalRateLimit,
141
+ body("code").custom((value) => {
142
+ if (!validateCode(value)) {
143
+ throw new Error("Invalid code format");
172
144
  }
173
- return sendSuccess(res, { assessments: application.assessments });
145
+ return true;
146
+ }),
147
+ handleValidationErrors,
148
+ requireTeacherAuth,
149
+ ], async (req, res) => {
150
+ try {
151
+ const { code } = req.body;
152
+ const db = getDatabase();
153
+ await deleteDelivery(db, code);
154
+ return sendSuccess(res, { code }, "Delivery deleted successfully");
174
155
  }
175
156
  catch (error) {
176
- console.error("Error retrieving assessments:", error);
177
- return sendError(res, 500, "Failed to retrieve assessments");
157
+ console.error("Error deleting delivery:", error);
158
+ return sendError(res, 500, "Failed to delete delivery");
178
159
  }
179
160
  });
180
161
  /**
181
162
  * @openapi
182
- * /teacher/students:
163
+ * /assessments:
183
164
  * get:
184
165
  * tags:
185
166
  * - Teacher
186
- * summary: Get planned sessions
167
+ * summary: Get all assessments
187
168
  * responses:
188
169
  * 200:
189
- * description: Returns planned sessions
170
+ * description: Returns all assessments
190
171
  * 401:
191
172
  * description: Unauthorized
173
+ * 404:
174
+ * description: Application not found
192
175
  * 500:
193
176
  * description: Internal server error
194
177
  */
195
- app.get("/teacher/students", [generalRateLimit, requireTeacherAuth], async (req, res) => {
178
+ teacherRouter.get("/assessments", [generalRateLimit, requireTeacherAuth], async (req, res) => {
196
179
  try {
197
- const db = getDatabase(req.appId);
198
- const studentSessionInfos = await getPlannedStudents(db, req.user.uid);
199
- return sendSuccess(res, studentSessionInfos);
180
+ const db = getDatabase();
181
+ const assessments = await getAssessments(db);
182
+ return sendSuccess(res, { assessments });
200
183
  }
201
184
  catch (error) {
202
- console.error("Error retrieving planned students:", error);
203
- return sendError(res, 500, "Failed to retrieve planned students");
204
- }
205
- });
206
- app.get("/teacher/assessment/:assessmentId/itemStats", [
207
- generalRateLimit,
208
- param("assessmentId").custom((value) => {
209
- if (!validateAssessmentId(value)) {
210
- throw new Error("Invalid assessment ID format");
211
- }
212
- return true;
213
- }),
214
- query("role").optional().isIn(["teacher", "reviewer"]),
215
- handleValidationErrors,
216
- requireTeacherAuth, // You might want to make this more generic
217
- ], async (req, res) => {
218
- try {
219
- const { assessmentId } = req.params;
220
- const role = req.query.role || "teacher"; // default to teacher
221
- const db = getDatabase(req.appId);
222
- const studentSessionInfos = await getItemsStats(db, req.user.uid, assessmentId, role);
223
- return sendSuccess(res, studentSessionInfos);
224
- }
225
- catch (error) {
226
- console.error("Error retrieving item stats:", error);
227
- return sendError(res, 500, "Failed to retrieve item statistics");
185
+ console.error("Error retrieving assessments:", error);
186
+ return sendError(res, 500, "Failed to retrieve assessments");
228
187
  }
229
188
  });
230
189
  /**
231
190
  * @openapi
232
- * /teacher/log:
191
+ * /delivery/stop:
233
192
  * post:
234
193
  * tags:
235
194
  * - Teacher
236
- * summary: Log teacher activity
237
- * description: Logs teacher activity data for debugging and analytics purposes.
195
+ * summary: Stop a delivery
238
196
  * requestBody:
239
197
  * required: true
240
198
  * content:
@@ -242,109 +200,92 @@ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImp
242
200
  * schema:
243
201
  * type: object
244
202
  * required:
245
- * - type
246
- * - data
203
+ * - code
247
204
  * properties:
248
- * type:
205
+ * code:
249
206
  * type: string
250
- * minLength: 1
251
- * maxLength: 100
252
- * description: The type/category of the log entry
253
- * data:
254
- * description: Any data to be logged
207
+ * description: The delivery code to stop
255
208
  * responses:
256
209
  * 200:
257
- * description: Successfully logged the data.
210
+ * description: Delivery stopped successfully
258
211
  * 400:
259
212
  * description: Invalid request data
260
213
  * 401:
261
214
  * description: Unauthorized
215
+ * 404:
216
+ * description: Delivery not found
262
217
  * 500:
263
218
  * description: Internal server error
264
219
  */
265
- app.post("/teacher/log", [
220
+ teacherRouter.post("/delivery/stop", [
266
221
  generalRateLimit,
267
- body("type")
268
- .isString()
269
- .trim()
270
- .isLength({ min: 1, max: 100 })
271
- .withMessage("Type must be a string between 1-100 characters"),
272
- body("data").exists().withMessage("Data field is required"),
222
+ body("code").custom((value) => {
223
+ if (!validateCode(value)) {
224
+ throw new Error("Invalid code format");
225
+ }
226
+ return true;
227
+ }),
273
228
  handleValidationErrors,
274
229
  requireTeacherAuth,
275
230
  ], async (req, res) => {
276
231
  try {
277
- const { type, data } = req.body;
278
- const db = getDatabase(req.appId);
279
- // Create log entry with timestamp and teacher ID
280
- const logEntry = {
281
- type,
282
- data,
283
- timestamp: new Date().toISOString(),
284
- teacherId: req.user.uid,
285
- appId: req.appId,
286
- };
232
+ const { code } = req.body;
233
+ const db = getDatabase();
287
234
  const firestore = getFirestore();
288
- await firestore
289
- .doc(db.TEACHER.LOG.DOC(req.user.uid, dateId()))
290
- .set(logEntry);
291
- return sendSuccess(res, undefined, "Log entry created successfully");
235
+ const deliveryRef = firestore.doc(db.DELIVERY.DOC(code));
236
+ const delivery = await deliveryRef.get();
237
+ if (!delivery.exists) {
238
+ return sendError(res, 404, "Delivery not found");
239
+ }
240
+ const finishedAt = new Date().getTime();
241
+ await deliveryRef.update({
242
+ modifiedAt: finishedAt,
243
+ finishedAt,
244
+ state: "inactive",
245
+ });
246
+ return sendSuccess(res, { code, finishedAt }, "Delivery stopped successfully");
292
247
  }
293
248
  catch (error) {
294
- console.error("Error creating log entry:", error);
295
- return sendError(res, 500, "Failed to create log entry");
249
+ console.error("Error stopping delivery:", error);
250
+ return sendError(res, 500, "Failed to stop delivery");
296
251
  }
297
252
  });
298
253
  /**
299
254
  * @openapi
300
- * /teacher/plan:
255
+ * /delivery/start:
301
256
  * post:
302
257
  * tags:
303
258
  * - Teacher
304
- * summary: Plan sessions
259
+ * summary: Restart a delivery
305
260
  * requestBody:
306
261
  * required: true
307
262
  * content:
308
263
  * application/json:
309
264
  * schema:
310
265
  * type: object
266
+ * required:
267
+ * - code
311
268
  * properties:
312
- * count:
313
- * type: number
314
- * minimum: 1
315
- * maximum: 100
316
- * assessmentIds:
317
- * type: array
318
- * items:
319
- * type: string
320
- * maxItems: 20
269
+ * code:
270
+ * type: string
271
+ * description: The delivery code to restart
321
272
  * responses:
322
273
  * 200:
323
- * description: Returns planned sessions
274
+ * description: Delivery restarted successfully
324
275
  * 400:
325
- * description: Invalid request data
276
+ * description: Invalid request data or delivery cannot be restarted
326
277
  * 401:
327
278
  * description: Unauthorized
328
279
  * 404:
329
- * description: Application not found
280
+ * description: Delivery not found
330
281
  * 500:
331
282
  * description: Internal server error
332
283
  */
333
- app.post("/teacher/plan", [
334
- heavyOperationLimit,
335
- body("count")
336
- .optional()
337
- .isInt({ min: 1, max: 100 })
338
- .withMessage("Count must be between 1 and 100"),
339
- body("assessmentIds")
340
- .optional()
341
- .isArray({ max: 20 })
342
- .withMessage("Assessment IDs must be an array with max 20 items"),
343
- body("assessmentIds.*")
344
- .optional()
345
- .custom((value) => {
346
- if (!validateAssessmentId(value)) {
347
- throw new Error("Invalid assessment ID format");
284
+ teacherRouter.post("/delivery/start", [
285
+ generalRateLimit,
286
+ body("code").custom((value) => {
287
+ if (!validateCode(value)) {
288
+ throw new Error("Invalid code format");
348
289
  }
349
290
  return true;
350
291
  }),
@@ -352,82 +293,51 @@ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImp
352
293
  requireTeacherAuth,
353
294
  ], async (req, res) => {
354
295
  try {
355
- const { count = 1, assessmentIds = [] } = req.body;
356
- const db = getDatabase(req.appId);
357
- const application = await getApplication(db);
358
- if (!application) {
359
- return sendError(res, 404, "Application not found");
296
+ const { code } = req.body;
297
+ const db = getDatabase();
298
+ const firestore = getFirestore();
299
+ const deliveryRef = firestore.doc(db.DELIVERY.DOC(code));
300
+ const delivery = await deliveryRef.get();
301
+ if (!delivery.exists) {
302
+ return sendError(res, 404, "Delivery not found");
360
303
  }
361
- const assessmentsToPlan = application.assessments.filter((a) => {
362
- return (assessmentIds.length === 0 || assessmentIds.includes(a.assessmentId));
304
+ await deliveryRef.update({
305
+ modifiedAt: new Date().getTime(),
306
+ finishedAt: null,
307
+ state: "active",
363
308
  });
364
- if (assessmentIds.length > 0 && assessmentsToPlan.length === 0) {
365
- return sendError(res, 400, "No valid assessments found for the provided IDs");
366
- }
367
- const studentSessionInfos = await createSession(req.user.uid, db, count, assessmentsToPlan);
368
- return sendSuccess(res, studentSessionInfos, "Sessions planned successfully");
309
+ return sendSuccess(res, { code }, "Delivery restarted successfully");
369
310
  }
370
311
  catch (error) {
371
- console.error("Error planning sessions:", error);
372
- return sendError(res, 500, "Failed to plan sessions");
312
+ console.error("Error restarting delivery:", error);
313
+ return sendError(res, 500, "Failed to restart delivery");
373
314
  }
374
315
  });
375
316
  /**
376
317
  * @openapi
377
- * /teacher/planByIdentification:
378
- * post:
318
+ * /assessment/{assessmentId}/deliveries:
319
+ * get:
379
320
  * tags:
380
321
  * - Teacher
381
- * summary: Plan sessions by identification e.g. student number
382
- * requestBody:
383
- * required: true
384
- * content:
385
- * application/json:
386
- * schema:
387
- * type: object
388
- * required:
389
- * - identifications
390
- * properties:
391
- * identifications:
392
- * type: array
393
- * items:
394
- * type: string
395
- * minItems: 1
396
- * maxItems: 100
397
- * assessmentIds:
398
- * type: array
399
- * items:
400
- * type: string
401
- * maxItems: 20
322
+ * summary: Get all deliveries for an assessment
323
+ * parameters:
324
+ * - in: path
325
+ * name: assessmentId
326
+ * required: true
327
+ * schema:
328
+ * type: string
329
+ * description: The assessment ID
402
330
  * responses:
403
331
  * 200:
404
- * description: Returns planned sessions
405
- * 400:
406
- * description: Invalid request data
332
+ * description: Returns all deliveries for the assessment
407
333
  * 401:
408
334
  * description: Unauthorized
409
- * 404:
410
- * description: Application not found
411
335
  * 500:
412
336
  * description: Internal server error
413
337
  */
414
- app.post("/teacher/planByIdentification", [
415
- heavyOperationLimit,
416
- body("identifications")
417
- .isArray({ min: 1, max: 100 })
418
- .withMessage("Identifications must be an array with 1-100 items"),
419
- body("identifications.*")
420
- .isString()
421
- .trim()
422
- .isLength({ min: 1, max: 100 })
423
- .withMessage("Each identification must be 1-100 characters"),
424
- body("assessmentIds")
425
- .optional()
426
- .isArray({ max: 20 })
427
- .withMessage("Assessment IDs must be an array with max 20 items"),
428
- body("assessmentIds.*")
429
- .optional()
430
- .custom((value) => {
338
+ teacherRouter.get("/assessment/:assessmentId/deliveries", [
339
+ generalRateLimit,
340
+ param("assessmentId").custom((value) => {
431
341
  if (!validateAssessmentId(value)) {
432
342
  throw new Error("Invalid assessment ID format");
433
343
  }
@@ -437,97 +347,126 @@ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImp
437
347
  requireTeacherAuth,
438
348
  ], async (req, res) => {
439
349
  try {
440
- const { identifications, assessmentIds = [] } = req.body;
441
- const db = getDatabase(req.appId);
442
- const application = await getApplication(db);
443
- if (!application) {
444
- return sendError(res, 404, "Application not found");
445
- }
446
- const assessmentsToPlan = application.assessments.filter((a) => {
447
- return (assessmentIds.length === 0 || assessmentIds.includes(a.assessmentId));
350
+ const { assessmentId } = req.params;
351
+ const db = getDatabase();
352
+ const firestore = getFirestore();
353
+ const deliveriesQuery = firestore
354
+ .collection(db.DELIVERY.COLLECTION())
355
+ .where("assessmentId", "==", assessmentId)
356
+ .where("createdBy", "==", req.user.uid);
357
+ const deliveriesSnapshot = await deliveriesQuery.get();
358
+ const deliveries = deliveriesSnapshot.docs.map((doc) => {
359
+ return doc.data();
448
360
  });
449
- if (assessmentIds.length > 0 && assessmentsToPlan.length === 0) {
450
- return sendError(res, 400, "No valid assessments found for the provided IDs");
451
- }
452
- const studentSessionInfos = await createSessionByIdentifications(req.user.uid, db, identifications.map((i) => ({ identifier: i })), "not_started", assessmentsToPlan);
453
- return sendSuccess(res, studentSessionInfos, "Sessions planned by identification successfully");
361
+ const sortedDeliveries = sort(deliveries, (d) => d.createdAt, true);
362
+ return sendSuccess(res, sortedDeliveries);
454
363
  }
455
364
  catch (error) {
456
- console.error("Error planning sessions by identification:", error);
457
- return sendError(res, 500, "Failed to plan sessions by identification");
365
+ console.error("Error retrieving deliveries:", error);
366
+ return sendError(res, 500, "Failed to retrieve deliveries");
458
367
  }
459
368
  });
460
369
  /**
461
370
  * @openapi
462
- * /teacher/assessment/:assessmentId/itemStats/:itemIdentifier:
463
- * post:
371
+ * /delivery/{assessmentId}/csv:
372
+ * get:
464
373
  * tags:
465
374
  * - Teacher
466
- * summary: Set score for an item response
375
+ * summary: Download results as CSV
467
376
  * parameters:
468
377
  * - in: path
469
- * name: itemIdentifier
378
+ * name: assessmentId
470
379
  * required: true
471
380
  * schema:
472
381
  * type: string
473
- * - in: path
474
- * name: assessmentId
475
- * required: true
382
+ * description: The assessment ID
383
+ * - in: query
384
+ * name: deliveryCode
385
+ * required: false
476
386
  * schema:
477
- * type: string
478
- * requestBody:
479
- * required: true
480
- * content:
481
- * application/json:
482
- * schema:
483
- * type: object
484
- * required:
485
- * - responseId
486
- * properties:
487
- * responseId:
488
- * type: string
489
- * scoreExternal:
490
- * oneOf:
491
- * - type: number
492
- * - type: string
493
- * - type: 'null'
494
- * target:
495
- * type: string
496
- * enum: [teacher, reviewer]
387
+ * type: string
388
+ * description: Optional delivery code to filter results
497
389
  * responses:
498
390
  * 200:
499
- * description: Score updated successfully
500
- * 400:
501
- * description: Invalid request data
391
+ * description: Returns CSV file with results
392
+ * content:
393
+ * text/csv:
394
+ * schema:
395
+ * type: string
396
+ * format: binary
502
397
  * 401:
503
398
  * description: Unauthorized
399
+ * 404:
400
+ * description: Assessment not found
504
401
  * 500:
505
402
  * description: Internal server error
506
403
  */
507
- app.post("/teacher/assessment/:assessmentId/itemStats/:itemIdentifier", [
404
+ teacherRouter.get("/delivery/:code/csv", [
508
405
  generalRateLimit,
509
- param("itemIdentifier")
510
- .isString()
511
- .trim()
512
- .isLength({ min: 1, max: 100 })
513
- .matches(/^[a-zA-Z0-9_.-]+$/)
514
- .withMessage("Item identifier must be alphanumeric with dots, dashes and underscores only"),
515
- param("assessmentId").custom((value) => {
516
- if (!validateAssessmentId(value)) {
517
- throw new Error("Invalid assessment ID format");
406
+ param("code").custom((value) => {
407
+ if (!validateCode(value)) {
408
+ throw new Error("Invalid code format");
518
409
  }
519
410
  return true;
520
411
  }),
521
- body("responseId")
522
- .isString()
523
- .trim()
524
- .isLength({ min: 1, max: 100 })
525
- .withMessage("Response ID is required and must be 1-100 characters"),
526
- body("target")
527
- .optional()
528
- .custom((value) => {
529
- if (value && !validateTarget(value)) {
530
- throw new Error('Target must be either "teacher" or "reviewer"');
412
+ handleValidationErrors,
413
+ requireTeacherAuth,
414
+ ], async (req, res) => {
415
+ try {
416
+ const { code } = req.params;
417
+ const db = getDatabase();
418
+ // Get assessment info
419
+ const assessment = await getAssessment(db, code);
420
+ if (!assessment) {
421
+ return sendError(res, 404, "Assessment not found");
422
+ }
423
+ // TODO: Implement actual results retrieval and CSV generation
424
+ // For now, return a placeholder CSV
425
+ const csvHeaders = "Assessment,DeliveryCode,StudentId,StudentName,StartTime,EndTime,Score,MaxScore,Percentage,Status\n";
426
+ const fileName = code
427
+ ? `${assessment.name}_${code}_results.csv`
428
+ : `${assessment.name}_all_results.csv`;
429
+ const csvContent = code
430
+ ? `${csvHeaders}${assessment.name},${code},,,,,,,No results yet\n`
431
+ : `${csvHeaders}${assessment.name},All,,,,,,,No results yet\n`;
432
+ res.setHeader("Content-Type", "text/csv");
433
+ res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`);
434
+ res.send(csvContent);
435
+ }
436
+ catch (error) {
437
+ console.error("Error downloading results:", error);
438
+ return sendError(res, 500, "Failed to download results");
439
+ }
440
+ });
441
+ /**
442
+ * @openapi
443
+ * /session/reset:
444
+ * post:
445
+ * tags:
446
+ * - Teacher
447
+ * summary: Reset session
448
+ * parameters:
449
+ * - in: query
450
+ * name: code
451
+ * required: true
452
+ * schema:
453
+ * type: string
454
+ * description: The session code
455
+ * responses:
456
+ * 200:
457
+ * description: Session reset successfully
458
+ * 401:
459
+ * description: Unauthorized
460
+ * 404:
461
+ * description: Session not found
462
+ * 500:
463
+ * description: Internal server error
464
+ */
465
+ teacherRouter.post("/session/reset", [
466
+ generalRateLimit,
467
+ param("code").custom((value) => {
468
+ if (!validateCode(value)) {
469
+ throw new Error("Invalid code format");
531
470
  }
532
471
  return true;
533
472
  }),
@@ -535,134 +474,128 @@ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImp
535
474
  requireTeacherAuth,
536
475
  ], async (req, res) => {
537
476
  try {
538
- const { assessmentId, itemIdentifier } = req.params;
539
- const { responseId, scoreExternal: scoreRawValue, target = "teacher", } = req.body;
540
- if (!validateTarget(target)) {
541
- return sendError(res, 400, 'Invalid target. Must be "teacher" or "reviewer"');
542
- }
543
- const scoreExternal = parseScore(scoreRawValue);
544
- const db = getDatabase(req.appId);
545
- await updateItemStatResponseScore(db, req.user.uid, assessmentId, itemIdentifier, responseId, scoreExternal, target);
546
- return sendSuccess(res, undefined, "Score updated successfully");
477
+ const code = req.params.code;
478
+ const userId = req.user.uid;
479
+ const db = getDatabase();
480
+ await resetSession(db, userId, code);
481
+ return sendSuccess(res, { message: "Session reset successfully" });
547
482
  }
548
483
  catch (error) {
549
- console.error("Error updating item score:", error);
550
- return sendError(res, 500, "Failed to update item score");
484
+ console.error("Error resetting session:", error);
485
+ return sendError(res, 500, "Failed to reset session");
551
486
  }
552
487
  });
553
488
  /**
554
- * @openapi
555
- * /teacher/access:
489
+ * /session/reopen:
556
490
  * post:
557
491
  * tags:
558
492
  * - Teacher
559
- * summary: Check if the teacher has access
560
- * description: Authenticates the teacher and checks if they have access.
493
+ * summary: Reopen session
494
+ * parameters:
495
+ * - in: query
496
+ * name: code
497
+ * required: true
498
+ * schema:
499
+ * type: string
500
+ * description: The session code
561
501
  * responses:
562
502
  * 200:
563
- * description: Returns if the teacher has access.
503
+ * description: Session reopened successfully
564
504
  * 401:
565
505
  * description: Unauthorized
506
+ * 404:
507
+ * description: Session not found
508
+ * 500:
509
+ * description: Internal server error
566
510
  */
567
- app.post("/teacher/access", [generalRateLimit, requireTeacherAuth], async (req, res) => {
568
- return sendSuccess(res, { hasAccess: true });
511
+ teacherRouter.post("/session/reopen", [
512
+ generalRateLimit,
513
+ param("code").custom((value) => {
514
+ if (!validateCode(value)) {
515
+ throw new Error("Invalid code format");
516
+ }
517
+ return true;
518
+ }),
519
+ handleValidationErrors,
520
+ requireTeacherAuth,
521
+ ], async (req, res) => {
522
+ try {
523
+ const code = req.params.code;
524
+ const userId = req.user.uid;
525
+ const db = getDatabase();
526
+ await reopenSession(db, userId, code);
527
+ return sendSuccess(res, { message: "Session reopened successfully" });
528
+ }
529
+ catch (error) {
530
+ console.error("Error resetting session:", error);
531
+ return sendError(res, 500, "Failed to reset session");
532
+ }
569
533
  });
570
534
  /**
571
535
  * @openapi
572
- * /teacher/session/update:
536
+ * /log:
573
537
  * post:
574
538
  * tags:
575
539
  * - Teacher
576
- * summary: Update a student session
577
- * description: Updates the session information for a student.
540
+ * summary: Log teacher activity
541
+ * description: Logs teacher activity data for debugging and analytics purposes.
578
542
  * requestBody:
579
543
  * required: true
580
544
  * content:
581
545
  * application/json:
582
546
  * schema:
583
547
  * type: object
584
- * required:
585
- * - assessmentId
586
- * - code
587
- * - session
588
548
  * properties:
589
- * assessmentId:
590
- * type: string
591
- * code:
549
+ * type:
592
550
  * type: string
593
- * session:
594
- * $ref: '#/components/schemas/SessionInfoTeacher'
551
+ * description: The type/category of the log entry
552
+ * data:
553
+ * description: Any data to be logged
595
554
  * responses:
596
555
  * 200:
597
- * description: Successfully updated the session.
598
- * 400:
599
- * description: Invalid request data
600
- * 401:
601
- * description: Unauthorized
556
+ * description: Successfully logged the data.
602
557
  * 500:
603
558
  * description: Internal server error
604
559
  */
605
- app.post("/teacher/session/update", [
560
+ teacherRouter.post("/log", [
606
561
  generalRateLimit,
607
- body("assessmentId").custom((value) => {
608
- if (!validateAssessmentId(value)) {
609
- throw new Error("Invalid assessment ID format");
610
- }
611
- return true;
612
- }),
613
- body("code").custom((value) => {
614
- if (!validateCode(value)) {
615
- throw new Error("Invalid code format");
616
- }
617
- return true;
618
- }),
619
- body("session").isObject().withMessage("Session must be an object"),
562
+ body("type").isString().withMessage("Type must be a string"),
563
+ body("data").exists().withMessage("Data field is required"),
620
564
  handleValidationErrors,
621
565
  requireTeacherAuth,
622
566
  ], async (req, res) => {
623
567
  try {
624
- const { assessmentId, code, session } = req.body;
625
- const db = getDatabase(req.appId);
626
- const firestore = getFirestore();
627
- const batch = firestore.batch();
628
- await Promise.all([
629
- specificImplementation.updatePlannedSession(req.user.uid, code, session, batch),
630
- updateStudentSessionState(db, code, session.assessmentId, session.sessionState, batch),
631
- ]);
632
- await batch.commit();
633
- return sendSuccess(res, undefined, "Session updated successfully");
568
+ const { type, data } = req.body;
569
+ // Implementation would depend on your logging requirements
570
+ return sendSuccess(res, undefined, "Log entry created successfully");
634
571
  }
635
572
  catch (error) {
636
- console.error("Error updating session:", error);
637
- return sendError(res, 500, "Failed to update session");
573
+ console.error("Error creating log entry:", error);
574
+ return sendError(res, 500, "Failed to create log entry");
638
575
  }
639
576
  });
640
577
  /**
641
578
  * @openapi
642
- * /teacher/session/{code}:
579
+ * /session/{code}:
643
580
  * delete:
644
581
  * tags:
645
582
  * - Teacher
646
- * summary: Delete a students session
647
- * description: Deletes a specific session for a student.
583
+ * summary: Delete student session
584
+ * description: Deletes a student session.
648
585
  * parameters:
649
586
  * - in: path
650
587
  * name: code
651
588
  * required: true
652
589
  * schema:
653
590
  * type: string
654
- * description: The session code.
591
+ * description: The session code
655
592
  * responses:
656
593
  * 200:
657
- * description: Successfully deleted the session.
658
- * 400:
659
- * description: Invalid code format
660
- * 401:
661
- * description: Unauthorized
594
+ * description: Successfully deleted student session.
662
595
  * 500:
663
596
  * description: Internal server error
664
597
  */
665
- app.delete("/teacher/session/:code", [
598
+ teacherRouter.delete("/session/:code", [
666
599
  generalRateLimit,
667
600
  param("code").custom((value) => {
668
601
  if (!validateCode(value)) {
@@ -675,48 +608,105 @@ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImp
675
608
  ], async (req, res) => {
676
609
  try {
677
610
  const { code } = req.params;
678
- const db = getDatabase(req.appId);
679
- await deleteStudent(db, req.user.uid, code);
680
- return sendSuccess(res, undefined, "Session deleted successfully");
611
+ const db = getDatabase();
612
+ const firestore = getFirestore();
613
+ // Delete the session document
614
+ await firestore.doc(db.SESSION.DOC(code.toUpperCase())).delete();
615
+ return sendSuccess(res, undefined, "Student session deleted successfully");
681
616
  }
682
617
  catch (error) {
683
- console.error("Error deleting session:", error);
684
- return sendError(res, 500, "Failed to delete session");
618
+ console.error("Error deleting student session:", error);
619
+ return sendError(res, 500, "Failed to delete student session");
685
620
  }
686
621
  });
687
622
  /**
688
623
  * @openapi
689
- * /teacher/session/reset:
624
+ * /student/update:
690
625
  * post:
691
626
  * tags:
692
627
  * - Teacher
693
- * summary: Reset a teacher's session
694
- * description: Resets the session information for a student.
628
+ * summary: Add student identification
629
+ * description: Updates student identification information.
630
+ * requestBody:
631
+ * required: true
632
+ * content:
633
+ * application/json:
634
+ * schema:
635
+ * type: object
636
+ * properties:
637
+ * code:
638
+ * type: string
639
+ * description: The session code
640
+ * identification:
641
+ * type: string
642
+ * description: The student identification
643
+ * responses:
644
+ * 200:
645
+ * description: Successfully updated student identification.
646
+ * 500:
647
+ * description: Internal server error
648
+ */
649
+ teacherRouter.post("/student/update", [
650
+ generalRateLimit,
651
+ body("code").custom((value) => {
652
+ if (!validateCode(value)) {
653
+ throw new Error("Invalid code format");
654
+ }
655
+ return true;
656
+ }),
657
+ body("identification")
658
+ .isString()
659
+ .withMessage("Identification must be a string"),
660
+ handleValidationErrors,
661
+ requireTeacherAuth,
662
+ ], async (req, res) => {
663
+ try {
664
+ const { code, identification } = req.body;
665
+ const db = getDatabase();
666
+ const firestore = getFirestore();
667
+ // Update the session with identification
668
+ await firestore.doc(db.SESSION.DOC(code.toUpperCase())).update({
669
+ identification,
670
+ updatedAt: new Date().getTime(),
671
+ });
672
+ return sendSuccess(res, undefined, "Student identification updated successfully");
673
+ }
674
+ catch (error) {
675
+ console.error("Error updating student identification:", error);
676
+ return sendError(res, 500, "Failed to update student identification");
677
+ }
678
+ });
679
+ /**
680
+ * @openapi
681
+ * /session/update:
682
+ * post:
683
+ * tags:
684
+ * - Teacher
685
+ * summary: Update session
686
+ * description: Updates session information.
695
687
  * requestBody:
696
688
  * required: true
697
689
  * content:
698
690
  * application/json:
699
691
  * schema:
700
692
  * type: object
701
- * required:
702
- * - code
703
- * - assessmentId
704
693
  * properties:
705
694
  * code:
706
695
  * type: string
696
+ * description: The session code
707
697
  * assessmentId:
708
698
  * type: string
699
+ * description: The assessment ID
700
+ * session:
701
+ * type: object
702
+ * description: The session data to update
709
703
  * responses:
710
704
  * 200:
711
- * description: Successfully reset the session.
712
- * 400:
713
- * description: Invalid request data
714
- * 401:
715
- * description: Unauthorized
705
+ * description: Successfully updated session.
716
706
  * 500:
717
707
  * description: Internal server error
718
708
  */
719
- app.post("/teacher/session/reset", [
709
+ teacherRouter.post("/session/update", [
720
710
  generalRateLimit,
721
711
  body("code").custom((value) => {
722
712
  if (!validateCode(value)) {
@@ -730,78 +720,227 @@ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImp
730
720
  }
731
721
  return true;
732
722
  }),
723
+ body("session").isObject().withMessage("Session must be an object"),
733
724
  handleValidationErrors,
734
725
  requireTeacherAuth,
735
726
  ], async (req, res) => {
736
727
  try {
737
- const { code, assessmentId } = req.body;
738
- const db = getDatabase(req.appId);
739
- await resetSession(db, req.user.uid, code, assessmentId);
740
- return sendSuccess(res, undefined, "Session reset successfully");
728
+ const { code, assessmentId, session } = req.body;
729
+ const db = getDatabase();
730
+ const firestore = getFirestore();
731
+ // Update the session
732
+ await firestore.doc(db.SESSION.DOC(code.toUpperCase())).update({
733
+ ...session,
734
+ assessmentId,
735
+ updatedAt: new Date().getTime(),
736
+ });
737
+ return sendSuccess(res, undefined, "Session updated successfully");
741
738
  }
742
739
  catch (error) {
743
- console.error("Error resetting session:", error);
744
- return sendError(res, 500, "Failed to reset session");
740
+ console.error("Error updating session:", error);
741
+ return sendError(res, 500, "Failed to update session");
745
742
  }
746
743
  });
747
744
  /**
748
745
  * @openapi
749
- * /teacher/student/update:
746
+ * /plan:
750
747
  * post:
751
748
  * tags:
752
749
  * - Teacher
753
- * summary: Update student information
754
- * description: Updates student information for a specific session.
750
+ * summary: Plan students
751
+ * description: Plans students for delivery.
755
752
  * requestBody:
756
753
  * required: true
757
754
  * content:
758
755
  * application/json:
759
756
  * schema:
760
757
  * type: object
761
- * required:
762
- * - code
763
- * - studentId
764
758
  * properties:
765
- * code:
766
- * type: string
767
- * studentId:
768
- * type: string
759
+ * count:
760
+ * type: number
761
+ * description: Number of students to plan
762
+ * deliveryCodes:
763
+ * type: array
764
+ * items:
765
+ * type: string
766
+ * description: Array of delivery codes
769
767
  * responses:
770
768
  * 200:
771
- * description: Successfully updated student information.
772
- * 400:
773
- * description: Invalid request data
774
- * 401:
775
- * description: Unauthorized
769
+ * description: Successfully planned students.
776
770
  * 500:
777
771
  * description: Internal server error
778
772
  */
779
- app.post("/teacher/student/update", [
773
+ teacherRouter.post("/plan", [
780
774
  generalRateLimit,
781
- body("code").custom((value) => {
782
- if (!validateCode(value)) {
783
- throw new Error("Invalid code format");
775
+ body("count")
776
+ .optional()
777
+ .isNumeric()
778
+ .withMessage("Count must be a number"),
779
+ body("deliveryCodes")
780
+ .optional()
781
+ .isArray()
782
+ .withMessage("Delivery codes must be an array"),
783
+ handleValidationErrors,
784
+ requireTeacherAuth,
785
+ ], async (req, res) => {
786
+ try {
787
+ const { count, deliveryCodes } = req.body;
788
+ // This would need to implement the planning logic
789
+ // For now, returning a placeholder implementation
790
+ return sendSuccess(res, [], "Students planned successfully");
791
+ }
792
+ catch (error) {
793
+ console.error("Error planning students:", error);
794
+ return sendError(res, 500, "Failed to plan students");
795
+ }
796
+ });
797
+ /**
798
+ * @openapi
799
+ * /students:
800
+ * get:
801
+ * tags:
802
+ * - Teacher
803
+ * summary: Get sessions
804
+ * description: Retrieves all sessions for the teacher.
805
+ * responses:
806
+ * 200:
807
+ * description: Successfully retrieved sessions.
808
+ * 500:
809
+ * description: Internal server error
810
+ */
811
+ teacherRouter.get("/students", [generalRateLimit, requireTeacherAuth], async (req, res) => {
812
+ try {
813
+ const db = getDatabase();
814
+ const firestore = getFirestore();
815
+ // Get sessions created by this teacher
816
+ const sessionsQuery = firestore
817
+ .collection(db.SESSION.COLLECTION())
818
+ .where("createdBy", "==", req.user.uid);
819
+ const sessionsSnapshot = await sessionsQuery.get();
820
+ const sessions = sessionsSnapshot.docs.map((doc) => doc.data());
821
+ return sendSuccess(res, sessions);
822
+ }
823
+ catch (error) {
824
+ console.error("Error retrieving sessions:", error);
825
+ return sendError(res, 500, "Failed to retrieve sessions");
826
+ }
827
+ });
828
+ /**
829
+ * @openapi
830
+ * /assessment/{assessmentId}/csv:
831
+ * get:
832
+ * tags:
833
+ * - Teacher
834
+ * summary: Download results by assessment ID
835
+ * description: Downloads results as CSV for a specific assessment.
836
+ * parameters:
837
+ * - in: path
838
+ * name: assessmentId
839
+ * required: true
840
+ * schema:
841
+ * type: string
842
+ * description: The assessment ID
843
+ * responses:
844
+ * 200:
845
+ * description: Successfully downloaded results.
846
+ * content:
847
+ * text/csv:
848
+ * schema:
849
+ * type: string
850
+ * format: binary
851
+ * 500:
852
+ * description: Internal server error
853
+ */
854
+ teacherRouter.get("/assessment/:assessmentId/csv", [
855
+ generalRateLimit,
856
+ param("assessmentId").custom((value) => {
857
+ if (!validateAssessmentId(value)) {
858
+ throw new Error("Invalid assessment ID format");
784
859
  }
785
860
  return true;
786
861
  }),
787
- body("identification")
788
- .isString()
789
- .trim()
790
- .isLength({ min: 1, max: 100 })
791
- .withMessage("identification must be 1-100 characters"),
792
862
  handleValidationErrors,
793
863
  requireTeacherAuth,
794
864
  ], async (req, res) => {
795
865
  try {
796
- const { code, identification } = req.body;
797
- const db = getDatabase(req.appId);
798
- await updateStudent(db, req.user.uid, code, identification);
799
- return sendSuccess(res, undefined, "Student updated successfully");
866
+ const { assessmentId } = req.params;
867
+ // This would need to implement the CSV export logic
868
+ // For now, returning a placeholder
869
+ res.setHeader("Content-Type", "text/csv");
870
+ res.setHeader("Content-Disposition", `attachment; filename="assessment-${assessmentId}.csv"`);
871
+ res.send("Assessment ID,Student ID,Score\n"); // Placeholder CSV
872
+ }
873
+ catch (error) {
874
+ console.error("Error downloading results:", error);
875
+ return sendError(res, 500, "Failed to download results");
876
+ }
877
+ });
878
+ /**
879
+ * @openapi
880
+ * /feedback:
881
+ * post:
882
+ * tags:
883
+ * - Teacher
884
+ * summary: Submit teacher feedback
885
+ * description: Endpoint for teachers to submit feedback with optional screenshot
886
+ * requestBody:
887
+ * required: true
888
+ * content:
889
+ * multipart/form-data:
890
+ * schema:
891
+ * type: object
892
+ * required:
893
+ * - type
894
+ * - description
895
+ * - feedbackId
896
+ * properties:
897
+ * type:
898
+ * type: string
899
+ * description: Type of feedback
900
+ * description:
901
+ * type: string
902
+ * description: Feedback description
903
+ * feedbackId:
904
+ * type: string
905
+ * description: Unique feedback identifier
906
+ * email:
907
+ * type: string
908
+ * description: User email (optional)
909
+ * pageUrl:
910
+ * type: string
911
+ * description: URL of the page where feedback was submitted
912
+ * screenshot:
913
+ * type: string
914
+ * format: binary
915
+ * description: Optional screenshot file
916
+ * responses:
917
+ * 200:
918
+ * description: Feedback submitted successfully
919
+ * 400:
920
+ * description: Missing required fields or validation error
921
+ * 401:
922
+ * description: Authentication required
923
+ * 500:
924
+ * description: Server error
925
+ */
926
+ teacherRouter.post("/feedback", [generalRateLimit, requireTeacherAuth], async (req, res) => {
927
+ try {
928
+ const userId = req.user.uid;
929
+ const { data, fileBuffer } = await processFeedbackSubmission(req);
930
+ // Validate the feedback data
931
+ const validationError = validateFeedbackData(data);
932
+ if (validationError) {
933
+ return sendError(res, 400, validationError);
934
+ }
935
+ // Save feedback with teacher user type
936
+ await saveFeedback(data, userId, 'teacher', fileBuffer);
937
+ return sendSuccess(res, undefined, "Feedback submitted successfully");
800
938
  }
801
939
  catch (error) {
802
- console.error("Error updating student:", error);
803
- return sendError(res, 500, "Failed to update student");
940
+ console.error("Error submitting teacher feedback:", error);
941
+ return sendError(res, 500, "An unexpected error occurred");
804
942
  }
805
943
  });
944
+ app.use("/teacher", teacherRouter);
806
945
  }
807
946
  //# sourceMappingURL=qti-teacher.js.map