@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
@@ -3,8 +3,10 @@ import fs from "fs/promises";
3
3
  import mime from "mime";
4
4
  import { createCode, removeDoubleSlashes } from "./utils";
5
5
  import { DATABASE } from "./database";
6
+ import { v4 as uuidv4 } from "uuid";
6
7
  import { deleteFilesRecursive } from "./storage";
7
8
  import NodeCache from "node-cache";
9
+ import Busboy from "busboy";
8
10
  export async function uploadFolderToStorage(folder, storageDestinationPath, uploadFiles, subfolder) {
9
11
  const storage = getStorage();
10
12
  const bucket = storage.bucket();
@@ -33,37 +35,71 @@ export async function uploadFolderToStorage(folder, storageDestinationPath, uplo
33
35
  }
34
36
  }
35
37
  }
36
- const applicationInfoCache = new NodeCache({
37
- stdTTL: 60 * 5, // 5 minutes
38
- });
39
38
  const userIdCode = new NodeCache({
40
39
  stdTTL: 60 * 20, // 20 minutes
41
40
  });
42
- export const getStudentSessionInfo = async (code, db, userId) => {
41
+ export const getTestsetSessions = async (testsetCode, db) => {
42
+ if (!testsetCode || !db)
43
+ return null;
44
+ const firestore = getFirestore();
45
+ const snapshot = await firestore
46
+ .doc(db.TESTSET_SESSIONS.DOC(testsetCode))
47
+ .get();
48
+ return snapshot.exists ? snapshot.data() : null;
49
+ };
50
+ export const getTestsetSessionWithSessions = async (testsetCode, db, userId) => {
51
+ if (!testsetCode || !db)
52
+ return null;
53
+ const firestore = getFirestore();
54
+ // Get the testset session from the testsetsessions collection
55
+ const testsetSessionDoc = await firestore
56
+ .doc(db.TESTSET_SESSIONS.DOC(testsetCode))
57
+ .get();
58
+ if (!testsetSessionDoc.exists) {
59
+ return null;
60
+ }
61
+ const testsetSession = testsetSessionDoc.data();
62
+ if (!testsetSession ||
63
+ !testsetSession.sessionIds ||
64
+ testsetSession.sessionIds.length === 0) {
65
+ return { ...testsetSession, sessions: [], userId };
66
+ }
67
+ // Retrieve all test sessions based on the sessionIds field
68
+ const sessionPromises = testsetSession.sessionIds.map((sessionId) => firestore.doc(db.SESSION.DOC(sessionId)).get());
69
+ const sessionDocs = await Promise.all(sessionPromises);
70
+ const sessions = sessionDocs
71
+ .filter((doc) => doc.exists)
72
+ .map((doc) => doc.data());
73
+ // Return the testset session with the populated sessions
74
+ return {
75
+ ...testsetSession,
76
+ sessions,
77
+ userId,
78
+ };
79
+ };
80
+ export const getSession = async (code, db, userId) => {
43
81
  if (!code || !db)
44
82
  return null;
45
- const application = await getApplication(db);
46
- if ((application?.demoCodes || []).includes(code)) {
47
- const allAssessments = await getAssessments(db);
83
+ const assessments = await getAssessments(db);
84
+ const matchAssessmentDemoId = assessments.find((a) => a.demoCode === code);
85
+ if (matchAssessmentDemoId) {
48
86
  return {
49
- appId: db.appId,
50
87
  code,
51
- sessions: allAssessments.map((a) => {
52
- return {
53
- assessmentId: a.assessmentId,
54
- assessmentName: a.name,
55
- packageId: a.packageId,
56
- sessionState: "not_started",
57
- };
58
- }),
59
- assessment: allAssessments.length ? allAssessments[0] : null,
60
- state: "not_started",
88
+ deliveryId: "<DEMO_DELIVERY_ID>",
89
+ assessmentId: matchAssessmentDemoId.id,
90
+ id: uuidv4(),
91
+ createdAt: new Date().getTime(),
92
+ updatedAt: new Date().getTime(),
93
+ createdBy: userId || null,
94
+ assessmentName: matchAssessmentDemoId.name,
95
+ packageId: matchAssessmentDemoId.packageId,
96
+ sessionState: "not_started",
61
97
  isDemo: true,
62
98
  teacherId: "demo-teacher",
63
99
  };
64
100
  }
65
101
  const firestore = getFirestore();
66
- const doc = await firestore.doc(db.STUDENT.DOC(code)).get();
102
+ const doc = await firestore.doc(db.SESSION.DOC(code)).get();
67
103
  if (userId && doc.exists) {
68
104
  const firestore = getFirestore();
69
105
  // store the mapping between the user and the code so on refresh we can get the code by the user
@@ -71,116 +107,26 @@ export const getStudentSessionInfo = async (code, db, userId) => {
71
107
  }
72
108
  return doc.exists ? doc.data() : null;
73
109
  };
74
- export const getStudentGroupSessionInfo = async (groupCode, db, userId, studentIdentification) => {
75
- if (!groupCode || !db || !userId)
110
+ export const getDelivery = async (code, db) => {
111
+ if (!code)
76
112
  return null;
77
113
  const firestore = getFirestore();
78
- // Check if the group code exists in the assessment codes collection
79
- const groupDeliveryDoc = await firestore
80
- .doc(db.GROUP_DELIVERY.DOC(groupCode))
81
- .get();
82
- if (!groupDeliveryDoc.exists) {
83
- return null;
84
- }
85
- const assessment = groupDeliveryDoc.data();
86
- const fullAssessment = await getAssessmentTest(db, assessment.assessmentId);
87
- // Create the mapping between user and code
88
- await firestore.doc(db.STUDENT_USER_ID_MAPPING.DOC(userId)).set({
89
- code: groupCode,
90
- });
91
- // Create/update student info entry
92
- const studentDocRef = firestore.doc(db.STUDENT.DOC(userId)); // Assuming nested structure
93
- const studentInfo = {
94
- appId: db.appId || "unknown",
95
- code: groupCode,
96
- currentAssessmentId: "",
97
- identification: studentIdentification || "",
98
- sessions: [],
99
- isDemo: false,
100
- teacherId: fullAssessment.teacherId || "unknown_teacher",
101
- userId: userId,
102
- };
103
- // Get assessment details
104
- const assessments = await getStartableAssessments(db);
105
- if (assessments && assessments.length > 0) {
106
- if (!studentInfo.currentAssessmentId) {
107
- studentInfo.currentAssessmentId = assessment.assessmentId;
108
- }
109
- studentInfo.sessions = [
110
- {
111
- assessmentId: assessment.assessmentId,
112
- assessmentName: assessment.name,
113
- packageId: assessment.packageId,
114
- sessionState: "not_started",
115
- },
116
- ];
117
- }
118
- await studentDocRef.set(studentInfo);
119
- return studentInfo;
114
+ const doc = await firestore.doc(db.DELIVERY.DOC(code)).get();
115
+ return doc.exists ? doc.data() : null;
120
116
  };
121
- export const updateStudentSessionState = async (db, code, assessmentId, state, batch) => {
117
+ export const updateSessionState = async (db, code, state, batch) => {
122
118
  if (!code || !db)
123
119
  return null;
124
120
  const firestore = getFirestore();
125
- const studentDocRef = firestore.doc(db.STUDENT.DOC(code));
126
- const student = (await studentDocRef.get())?.data();
127
- if (student) {
128
- const updatedStudent = {
129
- ...student,
130
- sessions: student.sessions.map((s) => s.assessmentId === assessmentId ? { ...s, sessionState: state } : s),
121
+ const sessionDocRef = firestore.doc(db.SESSION.DOC(code));
122
+ const session = (await sessionDocRef.get())?.data();
123
+ if (session) {
124
+ const updatedSession = {
125
+ ...session,
126
+ sessionState: state,
131
127
  };
132
- batch.set(studentDocRef, updatedStudent);
133
- }
134
- };
135
- export const createAssessmentCode = async (db, charCount, assessmentId, application) => {
136
- let code = createCode(charCount);
137
- let exists = !!(await codeExists(code, db));
138
- while (exists) {
139
- code = createCode(charCount);
140
- exists =
141
- (application?.demoCodes || []).includes(code) ||
142
- !!(await codeExists(code, db));
128
+ batch.set(sessionDocRef, updatedSession);
143
129
  }
144
- const firestore = getFirestore();
145
- const assessmentData = await firestore
146
- .doc(db.ASSESSMENT.DOC(assessmentId))
147
- .get();
148
- if (assessmentData.exists) {
149
- const assessmentInfo = assessmentData.data();
150
- await firestore.doc(db.GROUP_DELIVERY.DOC(code)).set(assessmentInfo);
151
- return code;
152
- }
153
- else {
154
- return null;
155
- }
156
- };
157
- export const createDemoCodeForAssessment = async (db, assessmentId, code) => {
158
- const exists = !!(await codeExists(code, db));
159
- if (exists) {
160
- return;
161
- }
162
- const firestore = getFirestore();
163
- const assessmentData = await firestore
164
- .doc(db.ASSESSMENT.DOC(assessmentId))
165
- .get();
166
- const assessmentInfo = assessmentData.data();
167
- await firestore
168
- .doc(db.GROUP_DELIVERY.DOC(code))
169
- .set({ ...assessmentInfo, isDemo: true });
170
- return code;
171
- };
172
- export const checkAssessmentCode = async (db, code) => {
173
- const firestore = getFirestore();
174
- if (!code || !db)
175
- return null;
176
- const application = await getApplication(db);
177
- if ((application?.demoCodes || []).includes(code)) {
178
- return {};
179
- }
180
- const assessmentInfo = await firestore.doc(db.GROUP_DELIVERY.DOC(code));
181
- const doc = await assessmentInfo.get();
182
- const assessment = doc.data();
183
- return assessment;
184
130
  };
185
131
  export const isStartable = (asessment) => {
186
132
  if (asessment.startFrom) {
@@ -197,184 +143,307 @@ export const isStartable = (asessment) => {
197
143
  }
198
144
  return true;
199
145
  };
200
- export const getCodeByUserId = async (db, userId) => {
201
- const key = `${db.appId}-${userId}`;
202
- const cached = userIdCode.get(key);
203
- if (cached) {
204
- return cached;
146
+ export const checkDeliveryCode = async (db, deliveryCode) => {
147
+ try {
148
+ const firestore = getFirestore();
149
+ const doc = await firestore.doc(db.DELIVERY.DOC(deliveryCode)).get();
150
+ return doc.exists;
205
151
  }
206
- const firestore = getFirestore();
207
- const doc = await firestore.doc(db.STUDENT_USER_ID_MAPPING.DOC(userId)).get();
208
- const code = doc.exists ? doc.data().code : null;
209
- if (code) {
210
- userIdCode.set(key, code);
152
+ catch (error) {
153
+ console.error("Error checking code:", error);
154
+ return false;
211
155
  }
212
- return code;
213
156
  };
214
- export const getTeacherIdByCode = async (db, code) => {
215
- const key = `${db.appId}-${code}`;
216
- const cached = userIdCode.get(key);
217
- if (cached) {
218
- return cached;
157
+ export const checkTestsetCode = async (db, testsetCode) => {
158
+ try {
159
+ const firestore = getFirestore();
160
+ const doc = await firestore.doc(db.TESTSET_SESSIONS.DOC(testsetCode)).get();
161
+ return doc.exists;
219
162
  }
220
- const firestore = getFirestore();
221
- const doc = await firestore.doc(db.STUDENT.DOC(code)).get();
222
- const teacherId = doc.exists
223
- ? doc.data().teacherId
224
- : null;
225
- if (teacherId) {
226
- userIdCode.set(key, teacherId);
163
+ catch (error) {
164
+ console.error("Error checking code:", error);
165
+ return false;
227
166
  }
228
- return teacherId;
229
167
  };
230
- export const getApplicationInfo = async (db) => {
231
- const cached = applicationInfoCache.get(db.appId || "application");
232
- if (cached) {
233
- return cached;
168
+ export const checkSessionCode = async (db, studentCode) => {
169
+ try {
170
+ const firestore = getFirestore();
171
+ const doc = await firestore.doc(db.SESSION.DOC(studentCode)).get();
172
+ return doc.exists;
234
173
  }
235
- const firestore = getFirestore();
236
- const doc = await firestore.doc(db.APPLICATION.DOC()).get();
237
- if (doc.exists) {
238
- const applicationInfo = doc.data();
239
- applicationInfoCache.set(db.appId || "application", applicationInfo);
240
- return applicationInfo;
174
+ catch (error) {
175
+ console.error("Error checking code:", error);
176
+ return false;
241
177
  }
242
178
  };
243
- export const addApplicationInfo = async (db, appInfo) => {
244
- const firestore = getFirestore();
245
- const docRef = firestore.doc(db.APPLICATION.DOC());
246
- await docRef.set(appInfo);
179
+ export const deleteDelivery = async (db, deliveryId) => {
180
+ try {
181
+ const firestore = getFirestore();
182
+ const docRef = firestore.doc(db.DELIVERY.DOC(deliveryId));
183
+ await docRef.delete();
184
+ }
185
+ catch (error) {
186
+ console.error("Error deleting delivery:", error);
187
+ }
247
188
  };
248
- export const getApplication = async (db) => {
249
- const firestore = getFirestore();
250
- const docRef = firestore.doc(db.APPLICATION.DOC());
251
- const applicationRef = await docRef.get();
252
- const application = applicationRef.exists
253
- ? applicationRef.data()
254
- : {
255
- name: db.appId,
189
+ export const createDelivery = async (db, userId, assessmentId) => {
190
+ try {
191
+ await getAssessment(db, assessmentId); // Ensure assessment exists
192
+ const firestore = getFirestore();
193
+ const docRef = firestore.collection(db.DELIVERY.COLLECTION()).doc();
194
+ const deliveryCode = await createUniqueCodeForDelivery(db);
195
+ const newDelivery = {
196
+ id: uuidv4(),
197
+ code: deliveryCode,
198
+ createdBy: userId,
199
+ state: "not_started",
200
+ assessmentId,
201
+ createdAt: new Date().getTime(),
202
+ updatedAt: new Date().getTime(),
256
203
  };
257
- if (application &&
258
- (!application.assessments || !application.assessments.length)) {
259
- const assessments = await getAssessments(db);
260
- if (!applicationRef.exists && assessments.length === 0) {
261
- return null;
262
- }
263
- application.assessments = assessments;
204
+ await docRef.set(newDelivery);
205
+ return docRef.id;
206
+ }
207
+ catch (error) {
208
+ console.error("Error creating delivery:", error);
209
+ return null;
264
210
  }
265
- return application;
266
- };
267
- export const codeExists = async (code, db) => {
268
- const studentSessions = await getStudentSessionInfo(code, db, undefined);
269
- return studentSessions;
270
211
  };
271
- export const hasAccess = async (uid, appId) => {
212
+ export const createSessionForDelivery = async (db, userId, delivery) => {
272
213
  const firestore = getFirestore();
273
- const docRef = firestore.doc(new DATABASE().AUTH.CHECK_APP_RIGHTS(uid, appId.toLowerCase()));
274
- const doc = await docRef.get();
275
- return doc.exists;
214
+ const assessment = await getAssessment(db, delivery.assessmentId);
215
+ const uniqueCode = await createUniqueCodeForStudentSession(db);
216
+ if (!uniqueCode) {
217
+ console.error("Failed to create unique code for student session");
218
+ return;
219
+ }
220
+ const session = {
221
+ assessmentId: delivery.assessmentId,
222
+ deliveryId: delivery.id,
223
+ createdBy: userId,
224
+ createdAt: new Date().getTime(),
225
+ updatedAt: new Date().getTime(),
226
+ assessmentName: assessment.name,
227
+ code: uniqueCode,
228
+ id: uniqueCode,
229
+ packageId: assessment.packageId,
230
+ sessionState: "not_started",
231
+ assessmentHref: assessment.assessmentHref,
232
+ };
233
+ await firestore.doc(db.SESSION.DOC(uniqueCode)).set(session);
234
+ return session;
276
235
  };
277
- // TODO: sessionState should be default on not_started. For snelkookpan specific move to specific logig
278
- export const createSession = async (teacherId, db, count, assessments) => {
236
+ export const testsetSessionCodeExists = async (code, db) => {
237
+ return checkTestsetCode(db, code);
238
+ };
239
+ export const sessionCodeExists = async (code, db) => {
240
+ return checkSessionCode(db, code);
241
+ };
242
+ export const deliveryCodeExists = async (code, db) => {
243
+ return checkDeliveryCode(db, code);
244
+ };
245
+ export const createTestsetSessionByIdentifications = async (teacherId, db, testsetCode, identifications,
246
+ // studentTestSessionCodes: { code: string; password?: string }[],
247
+ testsessionState = "not_started", sessionState = "not_started", createPassword = false) => {
279
248
  const firestore = getFirestore();
280
- const application = await getApplication(db);
281
- const codes = new Array(count).fill(createCode(5));
282
- const studentAppSessionInfos = [];
283
249
  const batch = firestore.batch();
284
- for (let code of codes) {
285
- // check if code does not exists twice in codes array
286
- const isGeneratedTwice = codes.filter((c) => c === code).length > 1;
287
- let exists = (await codeExists(code, db)) || isGeneratedTwice;
250
+ const allCreatedSessions = [];
251
+ const allCreatedTestsetSessions = [];
252
+ const assessments = [];
253
+ const testset = await getTestset(testsetCode, db);
254
+ for (const assessmentId of testset.assessmentIds) {
255
+ const assessment = await getAssessment(db, assessmentId);
256
+ if (assessment) {
257
+ assessments.push(assessment);
258
+ }
259
+ }
260
+ for (const identificationCode of identifications) {
261
+ // for (const studentCode of studentTestSessionCodes) {
262
+ const exists = await getTestsetSessions(identificationCode, db);
263
+ if (!exists) {
264
+ const password = createPassword ? createCode(4, "alphabetic") : undefined;
265
+ const testsetSession = {
266
+ id: identificationCode,
267
+ teacherId,
268
+ code: identificationCode,
269
+ createdAt: new Date().getTime(),
270
+ createdBy: teacherId,
271
+ updatedAt: new Date().getTime(),
272
+ state: testsessionState,
273
+ sessionIds: [],
274
+ password,
275
+ testsetId: testsetCode,
276
+ identification: "",
277
+ };
278
+ for (const assessmentId of testset.assessmentIds) {
279
+ const uniqueCode = await createUniqueCodeForStudentSession(db);
280
+ if (!uniqueCode) {
281
+ console.error("Failed to create unique code for student session");
282
+ return;
283
+ }
284
+ const assessment = assessments.find((a) => a.id === assessmentId);
285
+ const session = {
286
+ id: uniqueCode,
287
+ assessmentId,
288
+ deliveryId: teacherId,
289
+ createdBy: teacherId,
290
+ createdAt: new Date().getTime(),
291
+ updatedAt: new Date().getTime(),
292
+ assessmentName: assessment?.name || "",
293
+ packageId: assessment?.packageId || "",
294
+ teacherId,
295
+ assessmentHref: assessment?.assessmentHref,
296
+ testsetSessionId: identificationCode,
297
+ sessionState,
298
+ code: uniqueCode,
299
+ };
300
+ if (!testsetSession.sessionIds) {
301
+ testsetSession.sessionIds = [];
302
+ }
303
+ testsetSession.sessionIds.push(session.id);
304
+ batch.set(firestore.doc(db.SESSION.DOC(session.id)), session);
305
+ // now add them to the teacher view model
306
+ const teacherDocPath = db.TEACHER.SESSIONS.DOC(teacherId, session.code);
307
+ batch.set(firestore.doc(teacherDocPath), session);
308
+ allCreatedSessions.push(session);
309
+ }
310
+ batch.set(firestore.doc(db.TESTSET_SESSIONS.DOC(testsetSession.id)), testsetSession);
311
+ allCreatedTestsetSessions.push(testsetSession);
312
+ }
313
+ }
314
+ await batch.commit();
315
+ return {
316
+ session: allCreatedSessions,
317
+ testsetSessions: allCreatedTestsetSessions,
318
+ };
319
+ };
320
+ export const createUniqueCodeForDelivery = async (db) => {
321
+ // Create a unique delivery code if must be unique among students and deliveries
322
+ // each student session gets a unique code to be able to restart.
323
+ try {
324
+ let code = createCode(4);
325
+ let exists = await deliveryCodeExists(code, db);
326
+ while (exists) {
327
+ code = createCode(4);
328
+ exists = await deliveryCodeExists(code, db);
329
+ }
330
+ return code;
331
+ }
332
+ catch (error) {
333
+ console.error("Error creating activity:", error);
334
+ throw error;
335
+ }
336
+ };
337
+ export const createUniqueCodeForStudentSession = async (db) => {
338
+ // Create a unique delivery code if must be unique among students and deliveries
339
+ // each student session gets a unique code to be able to restart.
340
+ try {
341
+ let code = createCode(5);
342
+ let exists = await sessionCodeExists(code, db);
288
343
  while (exists) {
289
344
  code = createCode(5);
290
- exists = !!((application?.demoCodes || []).includes(code) ||
291
- (await codeExists(code, db)));
345
+ exists = await sessionCodeExists(code, db);
292
346
  }
293
- const studentSessionInfo = {
294
- teacherId,
295
- code,
296
- appId: db.appId || "application",
297
- sessions: assessments.map((assessment) => ({
298
- assessmentId: assessment.assessmentId,
299
- assessmentName: assessment.name,
300
- packageId: assessment.packageId,
301
- sessionState: assessment.assessmentId.startsWith("practice")
302
- ? "not_generated"
303
- : "not_available",
304
- })),
305
- isDemo: false,
306
- };
307
- studentAppSessionInfos.push(studentSessionInfo);
308
- const teacherResult = {
309
- code,
310
- sessions: studentSessionInfo.sessions.map((session) => ({
311
- code,
312
- assessmentId: session.assessmentId,
313
- sessionState: session.sessionState,
314
- testScore: 0,
315
- })),
316
- };
317
- batch.set(firestore.doc(db.STUDENT.DOC(code)), studentSessionInfo);
318
- // now add them to the teacher view model
319
- const teacherDocPath = db.TEACHER.STUDENTS.DOC(teacherId, teacherResult.code);
320
- batch.set(firestore.doc(teacherDocPath), teacherResult);
347
+ return code;
321
348
  }
322
- await batch.commit();
323
- return studentAppSessionInfos;
349
+ catch (error) {
350
+ console.error("Error creating activity:", error);
351
+ throw error;
352
+ }
353
+ };
354
+ export const createUniqueCodeForStudentTestsetSession = async (db) => {
355
+ // Create a unique delivery code if must be unique among students and deliveries
356
+ // each student session gets a unique code to be able to restart.
357
+ try {
358
+ let code = createCode(5);
359
+ let exists = await testsetSessionCodeExists(code, db);
360
+ while (exists) {
361
+ code = createCode(5);
362
+ exists = await testsetSessionCodeExists(code, db);
363
+ }
364
+ return code;
365
+ }
366
+ catch (error) {
367
+ console.error("Error creating activity:", error);
368
+ return null;
369
+ }
370
+ };
371
+ export const hasAccess = async (uid, appId) => {
372
+ // Always return true since we removed applicationId-based access control
373
+ return true;
324
374
  };
325
- export const createSessionByIdentifications = async (teacherId, db, identifications, sessionState, assessments) => {
375
+ export const createSessions = async (teacherId, deliveryCode, db, count, deliveryIds) => {
326
376
  const firestore = getFirestore();
327
- const studentAppSessionInfos = [];
328
377
  const batch = firestore.batch();
329
- for (const identification of identifications) {
330
- const exists = await codeExists(identification.identifier, db);
331
- if (!exists) {
332
- const studentSessionInfo = {
378
+ const sessionList = [];
379
+ const assessments = [];
380
+ for (const deliveryId of deliveryIds) {
381
+ const delivery = await getDelivery(deliveryCode, db);
382
+ if (!delivery) {
383
+ console.error(`Delivery with ID ${deliveryId} not found`);
384
+ continue;
385
+ }
386
+ let assessment = assessments.find((a) => a.id === delivery.assessmentId);
387
+ if (!assessment) {
388
+ assessment = await getAssessment(db, delivery.assessmentId);
389
+ if (assessment) {
390
+ assessments.push(assessment);
391
+ }
392
+ else {
393
+ console.error(`Assessment with ID ${delivery.assessmentId} not found`);
394
+ continue;
395
+ }
396
+ }
397
+ const codes = new Array(count).fill(createCode(5));
398
+ for (let code of codes) {
399
+ // check if code does not exists twice in codes array
400
+ const isGeneratedTwice = codes.filter((c) => c === code).length > 1;
401
+ let exists = (await deliveryCodeExists(code, db)) || isGeneratedTwice;
402
+ while (exists) {
403
+ code = createCode(5);
404
+ exists = !!(await deliveryCodeExists(code, db));
405
+ // TODO, we must check the assessment demo code as well.
406
+ // but for now these codes can be generated with at least one char that are not used in createCode,
407
+ // like AEOIU1
408
+ }
409
+ const id = uuidv4();
410
+ const plannedSessions = {
411
+ id,
412
+ code,
413
+ deliveryId,
333
414
  teacherId,
334
- code: identification.identifier,
335
- appId: db.appId || "application",
336
- sessions: assessments.map((assessment) => ({
337
- assessmentId: assessment.assessmentId,
338
- assessmentName: assessment.name,
339
- packageId: assessment.packageId,
340
- sessionState,
341
- })),
415
+ createdAt: new Date().getTime(),
416
+ updatedAt: new Date().getTime(),
417
+ createdBy: teacherId || "",
418
+ assessmentId: assessment.id,
419
+ assessmentName: assessment.name,
420
+ packageId: assessment.packageId,
421
+ sessionState: assessment.id.startsWith("practice")
422
+ ? "not_generated"
423
+ : "not_available",
342
424
  isDemo: false,
343
425
  };
344
- if (identification.password) {
345
- studentSessionInfo.password = identification.password;
346
- }
347
- studentAppSessionInfos.push(studentSessionInfo);
348
- const teacherResult = {
349
- code: identification.identifier,
350
- sessions: studentSessionInfo.sessions.map((session) => ({
351
- code: identification.identifier,
352
- assessmentId: session.assessmentId,
353
- sessionState: session.sessionState,
354
- testScore: 0,
355
- })),
356
- };
357
- batch.set(firestore.doc(db.STUDENT.DOC(identification.identifier)), studentSessionInfo);
358
- // now add them to the teacher view model
359
- const teacherDocPath = db.TEACHER.STUDENTS.DOC(teacherId, teacherResult.code);
360
- batch.set(firestore.doc(teacherDocPath), teacherResult);
426
+ sessionList.push(plannedSessions);
427
+ }
428
+ for (const session of sessionList) {
429
+ batch.set(firestore.doc(db.SESSION.DOC(session.code)), session);
430
+ batch.set(firestore.doc(db.TEACHER.SESSIONS.DOC(teacherId, session.code)), session);
361
431
  }
362
432
  }
363
433
  await batch.commit();
364
- return studentAppSessionInfos;
434
+ return sessionList;
365
435
  };
366
436
  export const getAllStudents = async (db) => {
367
437
  const firestore = getFirestore();
368
438
  const docs = await firestore
369
- .collection(db.STUDENT.COLLECTION())
439
+ .collection(db.SESSION.COLLECTION())
370
440
  .listDocuments();
371
- const studentInfos = [];
441
+ const studentSessions = [];
372
442
  for (const doc of docs) {
373
443
  const studentInfo = await doc.get();
374
- studentInfos.push(studentInfo.data() ||
375
- { code: doc.id });
444
+ studentSessions.push(studentInfo.data() || { code: doc.id });
376
445
  }
377
- return studentInfos;
446
+ return studentSessions;
378
447
  };
379
448
  export const getAssessments = async (db) => {
380
449
  const firestore = getFirestore();
@@ -384,29 +453,7 @@ export const getAssessments = async (db) => {
384
453
  const assessments = assessmentData.docs.map((doc) => doc.data());
385
454
  return assessments;
386
455
  };
387
- export const getStartableAssessments = async (db) => {
388
- const application = await getApplication(db);
389
- if (!application) {
390
- return [];
391
- }
392
- const startableAssessments = application.assessments.filter((a) => {
393
- if (a.startFrom) {
394
- const startFrom = new Date(a.startFrom);
395
- if (startFrom > new Date()) {
396
- return false;
397
- }
398
- }
399
- if (a.endAt) {
400
- const startUntil = new Date(a.endAt);
401
- if (startUntil < new Date()) {
402
- return false;
403
- }
404
- }
405
- return a.canStart !== false;
406
- });
407
- return startableAssessments;
408
- };
409
- export const getAssessmentTest = async (db, assessmentId) => {
456
+ export const getAssessment = async (db, assessmentId) => {
410
457
  const firestore = getFirestore();
411
458
  const assessmentDoc = await firestore
412
459
  .doc(db.ASSESSMENT.DOC(assessmentId))
@@ -420,6 +467,14 @@ export const getAssessmentTest = async (db, assessmentId) => {
420
467
  }
421
468
  return assessmentData;
422
469
  };
470
+ export const getTestset = async (testsetId, db) => {
471
+ const firestore = getFirestore();
472
+ const testsetDoc = await firestore.doc(db.TESTSET.DOC(testsetId)).get();
473
+ if (!testsetDoc.exists) {
474
+ throw new Error(`Testset with ID ${testsetId} does not exist.`);
475
+ }
476
+ return testsetDoc.data();
477
+ };
423
478
  export const getAssessmentItemRefsFromPackage = async ($assessmentTest) => {
424
479
  //let regularItemCount = 0;
425
480
  let itemCount = 0;
@@ -441,7 +496,7 @@ export const getAssessmentItemRefsFromPackage = async ($assessmentTest) => {
441
496
  };
442
497
  export const getItemsStats = async (db, teacherId, assessmentId, target) => {
443
498
  const firestore = getFirestore();
444
- const assessmentDoc = await getAssessmentTest(db, assessmentId);
499
+ const assessmentDoc = await getAssessment(db, assessmentId);
445
500
  if (!assessmentDoc) {
446
501
  throw new Error(`Assessment with ID ${assessmentId} does not exist.`);
447
502
  }
@@ -467,6 +522,7 @@ export const getItemsStats = async (db, teacherId, assessmentId, target) => {
467
522
  }
468
523
  else {
469
524
  const data = d.data();
525
+ const id = d.id;
470
526
  if (data) {
471
527
  let itemStats = stats.find((s) => s.itemId === itemIdentifier);
472
528
  if (!itemStats) {
@@ -481,7 +537,7 @@ export const getItemsStats = async (db, teacherId, assessmentId, target) => {
481
537
  itemStats.count += data.count;
482
538
  itemStats.numberCorrect +=
483
539
  !data.score || data.score?.toString() === "0" ? 0 : 1;
484
- itemStats.responses.push(data);
540
+ itemStats.responses.push({ ...data, id });
485
541
  }
486
542
  }
487
543
  }
@@ -499,44 +555,39 @@ export const updateItemStatResponseScore = async (db, teacherId, assessmentId, i
499
555
  export const getPlannedStudents = async (db, teacherId) => {
500
556
  const firestore = getFirestore();
501
557
  const studentResultCollectionRef = await firestore
502
- .collection(db.TEACHER.STUDENTS.COLLECTION(teacherId))
558
+ .collection(db.TEACHER.SESSIONS.COLLECTION(teacherId))
503
559
  .get();
504
560
  // StudentResult.
505
561
  const studentSessionInfo = studentResultCollectionRef.docs.map((doc) => doc.data());
506
- const studentsToReturn = [];
507
562
  let prevSessionState = "";
508
- for (const student of studentSessionInfo) {
509
- const sessionsToReturn = [];
510
- for (const session of student.sessions) {
511
- if (session.sessionState === "not_generated" &&
512
- session.assessmentId === "practice" &&
513
- prevSessionState === "scored") {
514
- session.sessionState = "not_available";
515
- }
516
- else if (session.assessmentId === "practice-2" &&
517
- (session.sessionState === "not_generated" ||
518
- session.sessionState === "not_available") &&
519
- prevSessionState === "scored") {
520
- session.sessionState = "not_started";
521
- }
522
- prevSessionState = session.sessionState;
523
- sessionsToReturn.push(session);
563
+ const sessionsToReturn = [];
564
+ for (const session of studentSessionInfo) {
565
+ if (session.sessionState === "not_generated" &&
566
+ session.assessmentId === "practice" &&
567
+ prevSessionState === "scored") {
568
+ session.sessionState = "not_available";
569
+ }
570
+ else if (session.assessmentId === "practice-2" &&
571
+ (session.sessionState === "not_generated" ||
572
+ session.sessionState === "not_available") &&
573
+ prevSessionState === "scored") {
574
+ session.sessionState = "not_started";
524
575
  }
525
- const updatedStudent = { ...student, sessions: sessionsToReturn };
526
- studentsToReturn.push(updatedStudent);
576
+ prevSessionState = session.sessionState;
577
+ sessionsToReturn.push(session);
527
578
  }
528
- return studentsToReturn;
579
+ return sessionsToReturn;
529
580
  };
530
581
  export const getPlannedStudent = async (db, teacherId, code) => {
531
582
  const firestore = getFirestore();
532
583
  const studentResultRef = await firestore
533
- .doc(db.TEACHER.STUDENTS.DOC(teacherId, code))
584
+ .doc(db.TEACHER.SESSIONS.DOC(teacherId, code))
534
585
  .get();
535
586
  return studentResultRef.data();
536
587
  };
537
588
  export const deleteStudent = async (db, teacherId, code) => {
538
589
  const firestore = getFirestore();
539
- const studentRef = await firestore.doc(db.TEACHER.STUDENTS.DOC(teacherId, code));
590
+ const studentRef = await firestore.doc(db.TEACHER.SESSIONS.DOC(teacherId, code));
540
591
  const assessments = await firestore
541
592
  .collection(db.ASSESSMENT.COLLECTION())
542
593
  .get();
@@ -557,61 +608,52 @@ export const setUser = async (uid, user) => {
557
608
  const firestore = getFirestore();
558
609
  await firestore.doc(new DATABASE().AUTH.DOC(uid)).set(user);
559
610
  };
560
- export const resetSession = async (db, teacherId, code, assessmentId) => {
611
+ export const reopenSession = async (db, teacherId, code) => {
561
612
  const firestore = getFirestore();
562
- const studentRef = await firestore
563
- .doc(db.TEACHER.STUDENTS.DOC(teacherId, code))
564
- .get();
565
- const student = studentRef.data();
566
- const updatedStudent = {
567
- ...student,
568
- sessions: student.sessions.map((s) => s.assessmentId === assessmentId
569
- ? { ...s, sessionState: "not_started" }
570
- : s),
571
- };
572
- await studentRef.ref.set(updatedStudent);
613
+ const batch = firestore.batch();
614
+ batch.update(firestore.doc(db.TEACHER.SESSIONS.DOC(teacherId, code)), {
615
+ sessionState: "not_started",
616
+ });
617
+ batch.update(firestore.doc(db.SESSION.DOC(code)), {
618
+ sessionState: "not_started",
619
+ });
620
+ await batch.commit();
621
+ };
622
+ export const resetSession = async (db, teacherId, code) => {
623
+ const firestore = getFirestore();
624
+ const batch = firestore.batch();
625
+ batch.update(firestore.doc(db.TEACHER.SESSIONS.DOC(teacherId, code)), {
626
+ sessionState: "not_started",
627
+ });
628
+ batch.update(firestore.doc(db.SESSION.DOC(code)), {
629
+ sessionState: "not_started",
630
+ });
631
+ batch.delete(firestore.doc(db.SESSION.DOC(code)));
632
+ await batch.commit();
573
633
  };
574
634
  export const getStudentInfoForTeacher = async (db, teacherId, code) => {
575
635
  const firestore = getFirestore();
576
636
  const studentRef = await firestore
577
- .doc(db.TEACHER.STUDENTS.DOC(teacherId, code))
637
+ .doc(db.TEACHER.SESSIONS.DOC(teacherId, code))
578
638
  .get();
579
639
  const student = studentRef.data();
580
640
  return student;
581
641
  };
582
- export const updateStudent = async (db, teacherId, code, identification) => {
642
+ export const updateStudentIdentification = async (db, teacherId, code, identification) => {
583
643
  const firestore = getFirestore();
584
- const studentRef = await firestore
585
- .doc(db.TEACHER.STUDENTS.DOC(teacherId, code))
586
- .get();
587
- const student = studentRef.data();
588
- const updatedStudent = {
589
- ...student,
644
+ const batch = firestore.batch();
645
+ batch.update(firestore.doc(db.TEACHER.SESSIONS.DOC(teacherId, code)), {
590
646
  identification,
591
- };
592
- await studentRef.ref.set(updatedStudent);
593
- };
594
- export const getAllApplications = async () => {
595
- const firestore = getFirestore();
596
- const applicationCollection = await firestore
597
- .collection(new DATABASE().APPLICATION.COLLECTION)
598
- .get();
599
- const applications = applicationCollection.docs.map((doc) => {
600
- const app = doc.data();
601
- if (!app.name) {
602
- app.name = doc.id;
603
- }
604
- return app;
605
647
  });
606
- return applications;
648
+ batch.update(firestore.doc(db.SESSION.DOC(code)), { identification });
649
+ await batch.commit();
607
650
  };
608
651
  const createStrongPassword = () => {
609
652
  const password = Math.random().toString(36).slice(-8);
610
653
  return password;
611
654
  };
612
- export const createUserForApplication = async (email, applicationId) => {
655
+ export const createUser = async (email) => {
613
656
  const auth = getAuth();
614
- const firestore = getFirestore();
615
657
  let password = "";
616
658
  let firebaseUser = undefined;
617
659
  try {
@@ -627,13 +669,7 @@ export const createUserForApplication = async (email, applicationId) => {
627
669
  password,
628
670
  });
629
671
  }
630
- await firestore
631
- .doc(new DATABASE().AUTH.CHECK_APP_RIGHTS(firebaseUser.uid, applicationId))
632
- .set({
633
- email,
634
- application: applicationId.toLocaleLowerCase(),
635
- userId: firebaseUser.uid,
636
- });
672
+ // No longer need to set application-specific permissions
637
673
  if (password) {
638
674
  return { password, id: firebaseUser.uid };
639
675
  }
@@ -641,61 +677,25 @@ export const createUserForApplication = async (email, applicationId) => {
641
677
  return { id: firebaseUser.uid };
642
678
  }
643
679
  };
644
- export const deleteUserForApplication = async (id, applicationId) => {
680
+ export const deleteUser = async (id) => {
645
681
  const auth = getAuth();
646
- const firestore = getFirestore();
647
- // check application right. If the user has rights to just this application then the user can be deleted.
648
- // otherwise we remove the user from firebase Authentication
649
- const applicationDocs = await firestore
650
- .collection(new DATABASE().AUTH.APP_COLLECTION(id))
651
- .listDocuments();
652
- const applicationIds = applicationDocs.map((a) => a.id.toLocaleLowerCase());
653
- if (!applicationIds.includes(applicationId.toLocaleLowerCase())) {
654
- return;
655
- }
656
- await firestore
657
- .doc(new DATABASE().AUTH.CHECK_APP_RIGHTS(id, applicationId))
658
- .delete();
659
- if (applicationIds.length === 1) {
660
- await auth.deleteUser(id);
661
- }
682
+ // Simply delete the user from Firebase Auth
683
+ await auth.deleteUser(id);
662
684
  return;
663
685
  };
664
- export const getAllApplicationsWithUsers = async () => {
665
- const firestore = getFirestore();
666
- const applications = await getAllApplications();
667
- const userDocs = await firestore
668
- .collection(new DATABASE().AUTH.COLLECTION)
669
- .listDocuments();
670
- const usersPerApplication = new Map();
671
- const allUserIds = userDocs.map((u) => u.id);
672
- const users = await fetchUsersInChunks(allUserIds);
673
- for (const userId of allUserIds) {
674
- const applicationsForUser = await firestore
675
- .collection(new DATABASE().AUTH.APP_COLLECTION(userId))
676
- .listDocuments();
677
- for (const applicationId of applicationsForUser.map((a) => a.id.toLowerCase())) {
678
- const userData = users.find((u) => u.id === userId);
679
- const user = {
680
- id: userId,
681
- email: userData?.email || "",
682
- disabled: userData?.disabled || false,
683
- };
684
- if (usersPerApplication.has(applicationId)) {
685
- usersPerApplication.get(applicationId)?.push(user);
686
- }
687
- else {
688
- usersPerApplication.set(applicationId, [user]);
689
- }
690
- }
691
- }
692
- const applicationWithUsers = applications.map((a) => {
693
- return {
694
- ...a,
695
- users: usersPerApplication.get(a.name?.toLocaleLowerCase()) || [],
696
- };
697
- });
698
- return applicationWithUsers;
686
+ export const getAllUsers = async () => {
687
+ const userDocs = await getAuth().listUsers();
688
+ const users = userDocs.users.map((user) => ({
689
+ id: user.uid,
690
+ email: user.email || "",
691
+ disabled: user.disabled || false,
692
+ }));
693
+ return [
694
+ {
695
+ name: "default",
696
+ users: users,
697
+ },
698
+ ];
699
699
  };
700
700
  export async function deleteDocumentAndSubcollections(docRef) {
701
701
  // Delete the document
@@ -719,7 +719,7 @@ export async function checkAndDeletePackages(dateTime) {
719
719
  const packages = packageData.docs.map((doc) => doc.data());
720
720
  for (const packageInfo of packages) {
721
721
  if (packageInfo.uploaded < dateTime) {
722
- for (const { assessmentId } of packageInfo.assessmentInfo) {
722
+ for (const { assessmentId } of packageInfo.Assessment) {
723
723
  await deleteDocumentAndSubcollections(firestore.doc(db.ASSESSMENT.DOC(assessmentId)));
724
724
  }
725
725
  await deleteFilesRecursive(storage.bucket(), `packages/${packageInfo.packageId}`);
@@ -749,11 +749,121 @@ export async function fetchUsersInChunks(ids, chunkSize = 100) {
749
749
  }
750
750
  return results;
751
751
  }
752
- export const getPackageIdByAssessment = async (db, code, assessmentId) => {
753
- const fs = getFirestore();
754
- const docRef = await fs.doc(db.STUDENT.DOC(code)).get();
755
- const studentInfo = docRef.data();
756
- const session = studentInfo.sessions.find((s) => s.assessmentId === assessmentId);
757
- return session?.packageId || null;
758
- };
752
+ /**
753
+ * Processes feedback submission with optional screenshot
754
+ * @param req - Express request object
755
+ * @param userId - User ID who is submitting feedback
756
+ * @param userType - Type of user ('teacher' or 'student')
757
+ * @returns Promise<{data: FeedbackSubmissionData, fileBuffer: Buffer | null, fileOriginalname: string | null}>
758
+ */
759
+ export function processFeedbackSubmission(req) {
760
+ return new Promise((resolve, reject) => {
761
+ const busboy = Busboy({ headers: req.headers });
762
+ const fields = {};
763
+ let fileBuffer = null;
764
+ let fileOriginalname = null;
765
+ busboy.on("field", (fieldname, val) => {
766
+ fields[fieldname] = val;
767
+ });
768
+ busboy.on("file", (fieldname, file, filename) => {
769
+ if (fieldname === "screenshot") {
770
+ const chunks = [];
771
+ fileOriginalname = filename.filename || filename;
772
+ file.on("data", (data) => {
773
+ chunks.push(data);
774
+ });
775
+ file.on("end", () => {
776
+ if (chunks.length) {
777
+ fileBuffer = Buffer.concat(chunks);
778
+ }
779
+ });
780
+ }
781
+ else {
782
+ // Discard other files
783
+ file.resume();
784
+ }
785
+ });
786
+ busboy.on("finish", () => {
787
+ const { type, description, feedbackId, email, pageUrl } = fields;
788
+ resolve({
789
+ data: { type, description, feedbackId, email, pageUrl },
790
+ fileBuffer,
791
+ fileOriginalname,
792
+ });
793
+ });
794
+ busboy.on("error", (error) => {
795
+ reject(error);
796
+ });
797
+ // Handle the request
798
+ if (req.rawBody) {
799
+ // For Firebase Functions
800
+ busboy.end(req.rawBody);
801
+ }
802
+ else {
803
+ // For regular Express
804
+ req.pipe(busboy);
805
+ }
806
+ });
807
+ }
808
+ /**
809
+ * Validates feedback submission data
810
+ * @param data - Feedback data to validate
811
+ * @returns Validation error message or null if valid
812
+ */
813
+ export function validateFeedbackData(data) {
814
+ const { type, description, feedbackId } = data;
815
+ if (!type || !description || !feedbackId) {
816
+ return "Missing required fields: type, description, and feedbackId are required";
817
+ }
818
+ if (type.length < 1 || type.length > 50) {
819
+ return "Type must be between 1 and 50 characters";
820
+ }
821
+ if (description.length < 1 || description.length > 2000) {
822
+ return "Description must be between 1 and 2000 characters";
823
+ }
824
+ if (feedbackId.length < 1 || feedbackId.length > 100) {
825
+ return "Feedback ID must be between 1 and 100 characters";
826
+ }
827
+ return null;
828
+ }
829
+ /**
830
+ * Saves feedback to Firestore and optionally uploads screenshot to Storage
831
+ * @param data - Feedback submission data
832
+ * @param userId - User ID who submitted the feedback
833
+ * @param userType - Type of user ('teacher' or 'student')
834
+ * @param fileBuffer - Screenshot file buffer (optional)
835
+ * @returns Promise<void>
836
+ */
837
+ export async function saveFeedback(data, userId, userType, fileBuffer) {
838
+ const { type, description, feedbackId, email, pageUrl } = data;
839
+ // Create feedback document in Firestore
840
+ const firestore = getFirestore();
841
+ const db = new DATABASE();
842
+ const firebaseDocPath = db.FEEDBACK.DOC(`${feedbackId}_${userId}`);
843
+ const feedbackRef = firestore.doc(firebaseDocPath);
844
+ await feedbackRef.set({
845
+ type,
846
+ description,
847
+ userId,
848
+ userType,
849
+ email: email || null,
850
+ pageUrl: pageUrl || null,
851
+ hasScreenshot: !!fileBuffer,
852
+ createdAt: new Date().toISOString(),
853
+ });
854
+ // If there's a screenshot, upload it to Firebase Storage
855
+ if (fileBuffer) {
856
+ const storage = getStorage();
857
+ const bucket = storage.bucket();
858
+ // storage path = firebaseDocPath but without the first /
859
+ const storagePath = firebaseDocPath.replace(/^\//, "");
860
+ const file = bucket.file(`${storagePath}.png`);
861
+ // Upload the file to Firebase Storage
862
+ await file.save(fileBuffer, {
863
+ metadata: {
864
+ contentType: "image/png",
865
+ },
866
+ });
867
+ }
868
+ }
759
869
  //# sourceMappingURL=logic.js.map