@hed-hog/lms 0.0.319 → 0.0.321

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 (128) hide show
  1. package/dist/class-group/class-group.controller.d.ts +64 -1
  2. package/dist/class-group/class-group.controller.d.ts.map +1 -1
  3. package/dist/class-group/class-group.controller.js +35 -0
  4. package/dist/class-group/class-group.controller.js.map +1 -1
  5. package/dist/class-group/class-group.service.d.ts +66 -1
  6. package/dist/class-group/class-group.service.d.ts.map +1 -1
  7. package/dist/class-group/class-group.service.js +164 -13
  8. package/dist/class-group/class-group.service.js.map +1 -1
  9. package/dist/class-group/dto/create-class-group.dto.d.ts.map +1 -1
  10. package/dist/class-group/dto/create-class-group.dto.js +2 -1
  11. package/dist/class-group/dto/create-class-group.dto.js.map +1 -1
  12. package/dist/class-group/dto/material.dto.d.ts +18 -0
  13. package/dist/class-group/dto/material.dto.d.ts.map +1 -0
  14. package/dist/class-group/dto/material.dto.js +86 -0
  15. package/dist/class-group/dto/material.dto.js.map +1 -0
  16. package/dist/course/course.service.d.ts +2 -0
  17. package/dist/course/course.service.d.ts.map +1 -1
  18. package/dist/course/course.service.js +27 -2
  19. package/dist/course/course.service.js.map +1 -1
  20. package/dist/course/dto/create-course.dto.d.ts +2 -2
  21. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  22. package/dist/course/dto/create-course.dto.js.map +1 -1
  23. package/dist/enterprise/enterprise.controller.d.ts +7 -1
  24. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  25. package/dist/enterprise/enterprise.controller.js +72 -2
  26. package/dist/enterprise/enterprise.controller.js.map +1 -1
  27. package/dist/enterprise/enterprise.module.d.ts.map +1 -1
  28. package/dist/enterprise/enterprise.module.js +2 -1
  29. package/dist/enterprise/enterprise.module.js.map +1 -1
  30. package/dist/enterprise/enterprise.service.d.ts +3 -0
  31. package/dist/enterprise/enterprise.service.d.ts.map +1 -1
  32. package/dist/enterprise/enterprise.service.js +84 -1
  33. package/dist/enterprise/enterprise.service.js.map +1 -1
  34. package/dist/enterprise/training/enterprise-training.module.d.ts +3 -0
  35. package/dist/enterprise/training/enterprise-training.module.d.ts.map +1 -0
  36. package/dist/enterprise/training/enterprise-training.module.js +40 -0
  37. package/dist/enterprise/training/enterprise-training.module.js.map +1 -0
  38. package/dist/enterprise/training/training-admin.controller.d.ts +525 -0
  39. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -0
  40. package/dist/enterprise/training/training-admin.controller.js +385 -0
  41. package/dist/enterprise/training/training-admin.controller.js.map +1 -0
  42. package/dist/enterprise/training/training-admin.service.d.ts +582 -0
  43. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -0
  44. package/dist/enterprise/training/training-admin.service.js +2283 -0
  45. package/dist/enterprise/training/training-admin.service.js.map +1 -0
  46. package/dist/enterprise/training/training-instructor.controller.d.ts +260 -0
  47. package/dist/enterprise/training/training-instructor.controller.d.ts.map +1 -0
  48. package/dist/enterprise/training/training-instructor.controller.js +199 -0
  49. package/dist/enterprise/training/training-instructor.controller.js.map +1 -0
  50. package/dist/enterprise/training/training-instructor.service.d.ts +280 -0
  51. package/dist/enterprise/training/training-instructor.service.d.ts.map +1 -0
  52. package/dist/enterprise/training/training-instructor.service.js +1218 -0
  53. package/dist/enterprise/training/training-instructor.service.js.map +1 -0
  54. package/dist/enterprise/training/training-student.controller.d.ts +168 -0
  55. package/dist/enterprise/training/training-student.controller.d.ts.map +1 -0
  56. package/dist/enterprise/training/training-student.controller.js +104 -0
  57. package/dist/enterprise/training/training-student.controller.js.map +1 -0
  58. package/dist/enterprise/training/training-student.service.d.ts +185 -0
  59. package/dist/enterprise/training/training-student.service.d.ts.map +1 -0
  60. package/dist/enterprise/training/training-student.service.js +674 -0
  61. package/dist/enterprise/training/training-student.service.js.map +1 -0
  62. package/dist/enterprise/training/training-viewer.controller.d.ts +298 -0
  63. package/dist/enterprise/training/training-viewer.controller.d.ts.map +1 -0
  64. package/dist/enterprise/training/training-viewer.controller.js +178 -0
  65. package/dist/enterprise/training/training-viewer.controller.js.map +1 -0
  66. package/dist/evaluation/dto/create-evaluation-topic.dto.d.ts +18 -0
  67. package/dist/evaluation/dto/create-evaluation-topic.dto.d.ts.map +1 -0
  68. package/dist/evaluation/dto/create-evaluation-topic.dto.js +59 -0
  69. package/dist/evaluation/dto/create-evaluation-topic.dto.js.map +1 -0
  70. package/dist/evaluation/dto/update-evaluation-topic.dto.d.ts +6 -0
  71. package/dist/evaluation/dto/update-evaluation-topic.dto.d.ts.map +1 -0
  72. package/dist/evaluation/dto/update-evaluation-topic.dto.js +9 -0
  73. package/dist/evaluation/dto/update-evaluation-topic.dto.js.map +1 -0
  74. package/dist/evaluation/evaluation.controller.d.ts +66 -0
  75. package/dist/evaluation/evaluation.controller.d.ts.map +1 -1
  76. package/dist/evaluation/evaluation.controller.js +73 -0
  77. package/dist/evaluation/evaluation.controller.js.map +1 -1
  78. package/dist/evaluation/evaluation.service.d.ts +71 -0
  79. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  80. package/dist/evaluation/evaluation.service.js +121 -0
  81. package/dist/evaluation/evaluation.service.js.map +1 -1
  82. package/dist/instructor/instructor.service.js +6 -6
  83. package/dist/instructor/instructor.service.js.map +1 -1
  84. package/dist/lms.module.d.ts.map +1 -1
  85. package/dist/lms.module.js +3 -0
  86. package/dist/lms.module.js.map +1 -1
  87. package/hedhog/data/menu.yaml +19 -2
  88. package/hedhog/data/route.yaml +730 -0
  89. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +74 -8
  90. package/hedhog/frontend/app/_components/course-avatar.tsx.ejs +10 -30
  91. package/hedhog/frontend/app/classes/[id]/_components/event-summary-popover.tsx.ejs +3 -3
  92. package/hedhog/frontend/app/classes/[id]/_components/quick-create-session-popover.tsx.ejs +1 -1
  93. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +2141 -308
  94. package/hedhog/frontend/app/classes/page.tsx.ejs +8 -7
  95. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +14 -1
  96. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +6 -2
  97. package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +201 -0
  98. package/hedhog/frontend/app/evaluations/_components/evaluation-topic-types.ts.ejs +49 -0
  99. package/hedhog/frontend/app/evaluations/page.tsx.ejs +483 -1112
  100. package/hedhog/frontend/app/instructors/page.tsx.ejs +22 -20
  101. package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +1278 -0
  102. package/hedhog/frontend/messages/en.json +98 -7
  103. package/hedhog/frontend/messages/pt.json +98 -7
  104. package/hedhog/table/course_class_group_material.yaml +45 -0
  105. package/package.json +8 -8
  106. package/src/class-group/class-group.controller.ts +30 -0
  107. package/src/class-group/class-group.service.ts +176 -5
  108. package/src/class-group/dto/create-class-group.dto.ts +2 -2
  109. package/src/class-group/dto/material.dto.ts +69 -0
  110. package/src/course/course.service.ts +41 -8
  111. package/src/course/dto/create-course.dto.ts +2 -2
  112. package/src/enterprise/enterprise.controller.ts +62 -1
  113. package/src/enterprise/enterprise.module.ts +2 -1
  114. package/src/enterprise/enterprise.service.ts +84 -1
  115. package/src/enterprise/training/enterprise-training.module.ts +27 -0
  116. package/src/enterprise/training/training-admin.controller.ts +278 -0
  117. package/src/enterprise/training/training-admin.service.ts +2523 -0
  118. package/src/enterprise/training/training-instructor.controller.ts +141 -0
  119. package/src/enterprise/training/training-instructor.service.ts +1303 -0
  120. package/src/enterprise/training/training-student.controller.ts +65 -0
  121. package/src/enterprise/training/training-student.service.ts +762 -0
  122. package/src/enterprise/training/training-viewer.controller.ts +115 -0
  123. package/src/evaluation/dto/create-evaluation-topic.dto.ts +48 -0
  124. package/src/evaluation/dto/update-evaluation-topic.dto.ts +6 -0
  125. package/src/evaluation/evaluation.controller.ts +63 -1
  126. package/src/evaluation/evaluation.service.ts +150 -1
  127. package/src/instructor/instructor.service.ts +4 -4
  128. package/src/lms.module.ts +3 -0
@@ -0,0 +1,1218 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __rest = (this && this.__rest) || function (s, e) {
12
+ var t = {};
13
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
14
+ t[p] = s[p];
15
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
16
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
17
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
18
+ t[p[i]] = s[p[i]];
19
+ }
20
+ return t;
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.TrainingInstructorService = void 0;
24
+ const api_prisma_1 = require("@hed-hog/api-prisma");
25
+ const core_1 = require("@hed-hog/core");
26
+ const common_1 = require("@nestjs/common");
27
+ let TrainingInstructorService = class TrainingInstructorService {
28
+ constructor(prisma, fileService) {
29
+ this.prisma = prisma;
30
+ this.fileService = fileService;
31
+ }
32
+ async getDashboard(userId, enterpriseId) {
33
+ const instructor = await this.prisma.instructor.findFirst({
34
+ where: { person: { person_user: { some: { user_id: userId } } } },
35
+ select: { id: true, person: { select: { name: true } } },
36
+ });
37
+ if (!instructor) {
38
+ throw new common_1.NotFoundException('No instructor profile found for this user');
39
+ }
40
+ const now = new Date();
41
+ const weekStart = this.startOfWeek(now);
42
+ const weekEnd = this.endOfDay(this.addDays(weekStart, 6));
43
+ const enterpriseFilter = enterpriseId !== undefined
44
+ ? { enterprise_class_group: { some: { enterprise_id: enterpriseId } } }
45
+ : {};
46
+ // Collect class group IDs where this instructor has sessions
47
+ const sessionRows = await this.prisma.course_class_session.findMany({
48
+ where: {
49
+ course_class_session_instructor: { some: { instructor_id: instructor.id } },
50
+ course_class_group: Object.assign({ status: { in: ['open', 'ongoing'] } }, enterpriseFilter),
51
+ },
52
+ select: { course_class_group_id: true },
53
+ distinct: ['course_class_group_id'],
54
+ });
55
+ const cgIds = sessionRows
56
+ .map((r) => r.course_class_group_id)
57
+ .filter((id) => id != null);
58
+ const [kpis, weeklyAgenda, myClasses, receivedFeedback] = await Promise.all([
59
+ this.getKpis(instructor.id, cgIds, weekStart, weekEnd),
60
+ this.getWeeklyAgenda(instructor.id, weekStart, weekEnd),
61
+ this.getMyClasses(cgIds, now),
62
+ this.getReceivedFeedbackSummary(instructor.id),
63
+ ]);
64
+ return {
65
+ kpis,
66
+ weeklyAgenda,
67
+ myClasses,
68
+ receivedFeedback,
69
+ };
70
+ }
71
+ // ── Private helpers ───────────────────────────────────────────────────────────
72
+ async getKpis(instructorId, cgIds, weekStart, weekEnd) {
73
+ var _a;
74
+ const [totalStudents, sessionsThisWeek, avgScore] = await Promise.all([
75
+ cgIds.length > 0
76
+ ? this.prisma.course_enrollment.count({
77
+ where: {
78
+ course_class_group_id: { in: cgIds },
79
+ status: { not: 'cancelled' },
80
+ },
81
+ })
82
+ : Promise.resolve(0),
83
+ this.prisma.course_class_session.count({
84
+ where: {
85
+ course_class_session_instructor: { some: { instructor_id: instructorId } },
86
+ session_date: { gte: weekStart, lte: weekEnd },
87
+ },
88
+ }),
89
+ this.prisma.exam_attempt.aggregate({
90
+ where: {
91
+ status: 'completed',
92
+ score: { not: null },
93
+ course_enrollment: {
94
+ course_class_group_id: cgIds.length > 0 ? { in: cgIds } : undefined,
95
+ },
96
+ },
97
+ _avg: { score: true },
98
+ }),
99
+ ]);
100
+ return {
101
+ activeClasses: {
102
+ value: cgIds.length,
103
+ change: null,
104
+ changeType: 'absolute',
105
+ },
106
+ totalStudents: {
107
+ value: totalStudents,
108
+ change: null,
109
+ changeType: 'absolute',
110
+ },
111
+ sessionsThisWeek: {
112
+ value: sessionsThisWeek,
113
+ change: null,
114
+ changeType: 'absolute',
115
+ },
116
+ averageScore: {
117
+ value: this.round((_a = avgScore._avg.score) !== null && _a !== void 0 ? _a : 0, 1),
118
+ change: null,
119
+ changeType: 'absolute',
120
+ },
121
+ };
122
+ }
123
+ async getWeeklyAgenda(instructorId, weekStart, weekEnd) {
124
+ const sessions = await this.prisma.course_class_session.findMany({
125
+ where: {
126
+ course_class_session_instructor: { some: { instructor_id: instructorId } },
127
+ session_date: { gte: weekStart, lte: weekEnd },
128
+ },
129
+ orderBy: [{ session_date: 'asc' }, { start_time: 'asc' }],
130
+ include: {
131
+ course_class_group: {
132
+ include: {
133
+ course: { select: { title: true } },
134
+ _count: {
135
+ select: {
136
+ course_enrollment: { where: { status: { not: 'cancelled' } } },
137
+ },
138
+ },
139
+ },
140
+ },
141
+ },
142
+ });
143
+ return sessions.map((s) => {
144
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
145
+ return ({
146
+ sessionId: s.id,
147
+ classGroupId: (_b = (_a = s.course_class_group) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null,
148
+ date: s.session_date,
149
+ time: s.start_time,
150
+ endTime: (_c = s.end_time) !== null && _c !== void 0 ? _c : null,
151
+ className: (_e = (_d = s.course_class_group) === null || _d === void 0 ? void 0 : _d.title) !== null && _e !== void 0 ? _e : null,
152
+ courseName: (_h = (_g = (_f = s.course_class_group) === null || _f === void 0 ? void 0 : _f.course) === null || _g === void 0 ? void 0 : _g.title) !== null && _h !== void 0 ? _h : null,
153
+ topic: (_j = s.topic) !== null && _j !== void 0 ? _j : null,
154
+ location: (_k = s.location) !== null && _k !== void 0 ? _k : null,
155
+ meetingUrl: (_l = s.meeting_url) !== null && _l !== void 0 ? _l : null,
156
+ deliveryMode: (_o = (_m = s.course_class_group) === null || _m === void 0 ? void 0 : _m.delivery_mode) !== null && _o !== void 0 ? _o : null,
157
+ studentCount: (_q = (_p = s.course_class_group) === null || _p === void 0 ? void 0 : _p._count.course_enrollment) !== null && _q !== void 0 ? _q : 0,
158
+ });
159
+ });
160
+ }
161
+ async getMyClasses(cgIds, now) {
162
+ if (cgIds.length === 0)
163
+ return [];
164
+ const classGroups = await this.prisma.course_class_group.findMany({
165
+ where: { id: { in: cgIds } },
166
+ orderBy: { start_date: 'desc' },
167
+ include: {
168
+ course: {
169
+ select: {
170
+ id: true,
171
+ title: true,
172
+ course_image: {
173
+ where: { image_type: { slug: 'course-logo' } },
174
+ orderBy: { is_primary: 'desc' },
175
+ take: 1,
176
+ select: { file: { select: { location: true } } },
177
+ },
178
+ },
179
+ },
180
+ _count: {
181
+ select: {
182
+ course_enrollment: { where: { status: { not: 'cancelled' } } },
183
+ },
184
+ },
185
+ course_class_session: {
186
+ where: { session_date: { gte: now } },
187
+ orderBy: { session_date: 'asc' },
188
+ take: 1,
189
+ select: { session_date: true, start_time: true, location: true },
190
+ },
191
+ },
192
+ });
193
+ return classGroups.map((cg) => {
194
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
195
+ const nextSession = (_a = cg.course_class_session[0]) !== null && _a !== void 0 ? _a : null;
196
+ const start = (_c = (_b = cg.start_date) === null || _b === void 0 ? void 0 : _b.getTime()) !== null && _c !== void 0 ? _c : 0;
197
+ const end = (_e = (_d = cg.end_date) === null || _d === void 0 ? void 0 : _d.getTime()) !== null && _e !== void 0 ? _e : 0;
198
+ const elapsed = end > start ? ((now.getTime() - start) / (end - start)) * 100 : 0;
199
+ return {
200
+ id: cg.id,
201
+ name: cg.title,
202
+ code: (_f = cg.code) !== null && _f !== void 0 ? _f : null,
203
+ courseId: (_h = (_g = cg.course) === null || _g === void 0 ? void 0 : _g.id) !== null && _h !== void 0 ? _h : null,
204
+ courseName: (_k = (_j = cg.course) === null || _j === void 0 ? void 0 : _j.title) !== null && _k !== void 0 ? _k : null,
205
+ courseLogoUrl: (_q = (_p = (_o = (_m = (_l = cg.course) === null || _l === void 0 ? void 0 : _l.course_image) === null || _m === void 0 ? void 0 : _m[0]) === null || _o === void 0 ? void 0 : _o.file) === null || _p === void 0 ? void 0 : _p.location) !== null && _q !== void 0 ? _q : null,
206
+ studentCount: cg._count.course_enrollment,
207
+ nextSession: nextSession
208
+ ? {
209
+ date: nextSession.session_date,
210
+ time: nextSession.start_time,
211
+ location: (_r = nextSession.location) !== null && _r !== void 0 ? _r : null,
212
+ }
213
+ : null,
214
+ progress: this.round(Math.min(Math.max(elapsed, 0), 100), 0),
215
+ status: cg.status,
216
+ deliveryMode: (_s = cg.delivery_mode) !== null && _s !== void 0 ? _s : null,
217
+ pendingEvaluations: 0, // populated separately if needed
218
+ };
219
+ });
220
+ }
221
+ async getReceivedFeedbackSummary(instructorId) {
222
+ var _a, _b;
223
+ // Find session IDs taught by this instructor
224
+ const sessionRows = await this.prisma.course_class_session.findMany({
225
+ where: {
226
+ course_class_session_instructor: { some: { instructor_id: instructorId } },
227
+ },
228
+ select: {
229
+ id: true,
230
+ title: true,
231
+ session_date: true,
232
+ course_class_group: { select: { title: true } },
233
+ },
234
+ orderBy: { session_date: 'desc' },
235
+ });
236
+ const sessionIds = sessionRows.map((s) => s.id);
237
+ if (sessionIds.length === 0) {
238
+ return { totalRatings: 0, averageScore: 0, recentItems: [] };
239
+ }
240
+ // Fetch evaluation topics for these sessions
241
+ const topics = await this.prisma.evaluation_topic.findMany({
242
+ where: {
243
+ course_class_session_id: { in: sessionIds },
244
+ target_type: 'course_class_session',
245
+ is_active: true,
246
+ },
247
+ include: {
248
+ evaluation_rating: { select: { score: true } },
249
+ course_class_session: {
250
+ select: {
251
+ id: true,
252
+ title: true,
253
+ session_date: true,
254
+ course_class_group: { select: { title: true } },
255
+ },
256
+ },
257
+ },
258
+ });
259
+ // Aggregate per session
260
+ const sessionMap = new Map();
261
+ for (const topic of topics) {
262
+ const sess = topic.course_class_session;
263
+ if (!sess)
264
+ continue;
265
+ if (!sessionMap.has(sess.id)) {
266
+ sessionMap.set(sess.id, {
267
+ sessionTitle: sess.title,
268
+ className: (_b = (_a = sess.course_class_group) === null || _a === void 0 ? void 0 : _a.title) !== null && _b !== void 0 ? _b : null,
269
+ sessionDate: sess.session_date,
270
+ scores: [],
271
+ });
272
+ }
273
+ for (const r of topic.evaluation_rating) {
274
+ sessionMap.get(sess.id).scores.push(Number(r.score));
275
+ }
276
+ }
277
+ const allScores = [...sessionMap.values()].flatMap((v) => v.scores);
278
+ const totalRatings = allScores.length;
279
+ const averageScore = totalRatings > 0 ? this.round(allScores.reduce((a, b) => a + b, 0) / totalRatings, 1) : 0;
280
+ // Recent items — last 5 sessions with at least 1 rating, sorted desc
281
+ const recentItems = [...sessionMap.entries()]
282
+ .filter(([, v]) => v.scores.length > 0)
283
+ .sort(([, a], [, b]) => b.sessionDate.getTime() - a.sessionDate.getTime())
284
+ .slice(0, 5)
285
+ .map(([, v]) => ({
286
+ sessionTitle: v.sessionTitle,
287
+ className: v.className,
288
+ sessionDate: v.sessionDate,
289
+ avgScore: this.round(v.scores.reduce((a, b) => a + b, 0) / v.scores.length, 1),
290
+ ratingCount: v.scores.length,
291
+ }));
292
+ return { totalRatings, averageScore, recentItems };
293
+ }
294
+ async getPendingEvals(cgIds) {
295
+ if (cgIds.length === 0)
296
+ return [];
297
+ const attempts = await this.prisma.exam_attempt.findMany({
298
+ where: {
299
+ status: 'completed',
300
+ score: null,
301
+ course_enrollment: { course_class_group_id: { in: cgIds } },
302
+ },
303
+ take: 20,
304
+ orderBy: { finished_at: 'asc' },
305
+ include: {
306
+ course_enrollment: {
307
+ include: {
308
+ person: { select: { name: true } },
309
+ course_class_group: { select: { title: true } },
310
+ },
311
+ },
312
+ exam: { select: { id: true, title: true } },
313
+ },
314
+ });
315
+ return attempts.map((a) => {
316
+ var _a, _b, _c, _d, _e, _f, _g, _h;
317
+ return ({
318
+ attemptId: a.id,
319
+ studentName: (_c = (_b = (_a = a.course_enrollment) === null || _a === void 0 ? void 0 : _a.person) === null || _b === void 0 ? void 0 : _b.name) !== null && _c !== void 0 ? _c : null,
320
+ className: (_f = (_e = (_d = a.course_enrollment) === null || _d === void 0 ? void 0 : _d.course_class_group) === null || _e === void 0 ? void 0 : _e.title) !== null && _f !== void 0 ? _f : null,
321
+ examTitle: (_h = (_g = a.exam) === null || _g === void 0 ? void 0 : _g.title) !== null && _h !== void 0 ? _h : null,
322
+ submittedAt: a.finished_at,
323
+ });
324
+ });
325
+ }
326
+ // ── Date helpers ──────────────────────────────────────────────────────────────
327
+ async getClassGroups(userId, options = {}) {
328
+ const instructor = await this.prisma.instructor.findFirst({
329
+ where: { person: { person_user: { some: { user_id: userId } } } },
330
+ select: { id: true },
331
+ });
332
+ if (!instructor) {
333
+ throw new common_1.NotFoundException('No instructor profile found for this user');
334
+ }
335
+ const { enterpriseId, search, status, deliveryMode } = options;
336
+ const now = new Date();
337
+ // IDs where instructor leads at least one session
338
+ const sessionRows = await this.prisma.course_class_session.findMany({
339
+ where: {
340
+ course_class_session_instructor: { some: { instructor_id: instructor.id } },
341
+ },
342
+ select: { course_class_group_id: true },
343
+ distinct: ['course_class_group_id'],
344
+ });
345
+ const cgIdsFromSessions = sessionRows
346
+ .map((r) => r.course_class_group_id)
347
+ .filter((id) => id != null);
348
+ const classGroups = await this.prisma.course_class_group.findMany({
349
+ where: {
350
+ AND: [
351
+ // instructor of the group OR instructor of at least one session
352
+ {
353
+ OR: [
354
+ { instructor_id: instructor.id },
355
+ { id: { in: cgIdsFromSessions } },
356
+ ],
357
+ },
358
+ // enterprise filter
359
+ ...(enterpriseId !== undefined
360
+ ? [{ enterprise_class_group: { some: { enterprise_id: enterpriseId } } }]
361
+ : []),
362
+ // status filter
363
+ ...(status ? [{ status: status }] : []),
364
+ // delivery mode filter
365
+ ...(deliveryMode ? [{ delivery_mode: deliveryMode }] : []),
366
+ // search filter
367
+ ...(search
368
+ ? [
369
+ {
370
+ OR: [
371
+ { title: { contains: search, mode: 'insensitive' } },
372
+ { code: { contains: search, mode: 'insensitive' } },
373
+ { course: { title: { contains: search, mode: 'insensitive' } } },
374
+ ],
375
+ },
376
+ ]
377
+ : []),
378
+ ],
379
+ },
380
+ orderBy: { start_date: 'desc' },
381
+ include: {
382
+ course: {
383
+ select: {
384
+ id: true,
385
+ title: true,
386
+ course_image: {
387
+ where: { image_type: { slug: { in: ['course-logo', 'course-banner'] } } },
388
+ orderBy: { is_primary: 'desc' },
389
+ select: {
390
+ file: { select: { location: true } },
391
+ image_type: { select: { slug: true } },
392
+ },
393
+ },
394
+ },
395
+ },
396
+ _count: {
397
+ select: {
398
+ course_enrollment: { where: { status: { not: 'cancelled' } } },
399
+ },
400
+ },
401
+ course_class_session: {
402
+ where: { session_date: { gte: now } },
403
+ orderBy: { session_date: 'asc' },
404
+ take: 1,
405
+ select: {
406
+ session_date: true,
407
+ start_time: true,
408
+ location: true,
409
+ meeting_url: true,
410
+ },
411
+ },
412
+ exam: {
413
+ select: {
414
+ id: true,
415
+ exam_attempt: {
416
+ where: { status: 'completed', score: null },
417
+ select: { id: true },
418
+ },
419
+ },
420
+ },
421
+ },
422
+ });
423
+ const data = classGroups.map((cg) => {
424
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w;
425
+ const start = (_b = (_a = cg.start_date) === null || _a === void 0 ? void 0 : _a.getTime()) !== null && _b !== void 0 ? _b : 0;
426
+ const end = (_d = (_c = cg.end_date) === null || _c === void 0 ? void 0 : _c.getTime()) !== null && _d !== void 0 ? _d : 0;
427
+ const elapsed = end > start ? ((now.getTime() - start) / (end - start)) * 100 : 0;
428
+ const nextSession = (_e = cg.course_class_session[0]) !== null && _e !== void 0 ? _e : null;
429
+ const pendingEvaluations = cg.exam.reduce((sum, ex) => sum + ex.exam_attempt.length, 0);
430
+ return {
431
+ id: cg.id,
432
+ name: cg.title,
433
+ code: (_f = cg.code) !== null && _f !== void 0 ? _f : '',
434
+ courseId: (_h = (_g = cg.course) === null || _g === void 0 ? void 0 : _g.id) !== null && _h !== void 0 ? _h : null,
435
+ courseName: (_k = (_j = cg.course) === null || _j === void 0 ? void 0 : _j.title) !== null && _k !== void 0 ? _k : null,
436
+ courseLogoUrl: (_p = (_o = (_m = (_l = cg.course) === null || _l === void 0 ? void 0 : _l.course_image.find((ci) => { var _a; return ((_a = ci.image_type) === null || _a === void 0 ? void 0 : _a.slug) === 'course-logo'; })) === null || _m === void 0 ? void 0 : _m.file) === null || _o === void 0 ? void 0 : _o.location) !== null && _p !== void 0 ? _p : null,
437
+ courseBannerUrl: (_t = (_s = (_r = (_q = cg.course) === null || _q === void 0 ? void 0 : _q.course_image.find((ci) => { var _a; return ((_a = ci.image_type) === null || _a === void 0 ? void 0 : _a.slug) === 'course-banner'; })) === null || _r === void 0 ? void 0 : _r.file) === null || _s === void 0 ? void 0 : _s.location) !== null && _t !== void 0 ? _t : null,
438
+ studentCount: cg._count.course_enrollment,
439
+ maxStudents: cg.capacity,
440
+ nextSession: nextSession
441
+ ? {
442
+ date: nextSession.session_date,
443
+ time: nextSession.start_time,
444
+ location: (_u = nextSession.location) !== null && _u !== void 0 ? _u : null,
445
+ meetingUrl: (_v = nextSession.meeting_url) !== null && _v !== void 0 ? _v : null,
446
+ }
447
+ : null,
448
+ progress: this.round(Math.min(Math.max(elapsed, 0), 100), 0),
449
+ status: cg.status,
450
+ deliveryMode: cg.delivery_mode,
451
+ startDate: cg.start_date,
452
+ endDate: (_w = cg.end_date) !== null && _w !== void 0 ? _w : null,
453
+ pendingEvaluations,
454
+ };
455
+ });
456
+ return { data, total: data.length };
457
+ }
458
+ // ── Class-group detail endpoints ─────────────────────────────────────────────
459
+ async assertInstructorOwnsClass(userId, classGroupId) {
460
+ const instructor = await this.prisma.instructor.findFirst({
461
+ where: { person: { person_user: { some: { user_id: userId } } } },
462
+ select: { id: true },
463
+ });
464
+ if (!instructor)
465
+ throw new common_1.NotFoundException('No instructor profile found for this user');
466
+ const cg = await this.prisma.course_class_group.findFirst({
467
+ where: {
468
+ id: classGroupId,
469
+ OR: [
470
+ { instructor_id: instructor.id },
471
+ {
472
+ course_class_session: {
473
+ some: {
474
+ course_class_session_instructor: { some: { instructor_id: instructor.id } },
475
+ },
476
+ },
477
+ },
478
+ ],
479
+ },
480
+ select: { id: true },
481
+ });
482
+ if (!cg)
483
+ throw new common_1.NotFoundException('Class group not found or not accessible to this instructor');
484
+ return { instructorId: instructor.id };
485
+ }
486
+ async getClassGroupDetail(userId, classGroupId) {
487
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
488
+ await this.assertInstructorOwnsClass(userId, classGroupId);
489
+ const now = new Date();
490
+ const cg = await this.prisma.course_class_group.findUnique({
491
+ where: { id: classGroupId },
492
+ include: {
493
+ course: { select: { id: true, title: true } },
494
+ instructor: { select: { person: { select: { name: true } } } },
495
+ _count: {
496
+ select: {
497
+ course_enrollment: { where: { status: { not: 'cancelled' } } },
498
+ course_class_session: true,
499
+ },
500
+ },
501
+ course_class_session: {
502
+ where: { session_date: { gte: now } },
503
+ orderBy: { session_date: 'asc' },
504
+ take: 1,
505
+ select: { session_date: true, start_time: true, location: true, meeting_url: true },
506
+ },
507
+ },
508
+ });
509
+ if (!cg)
510
+ throw new common_1.NotFoundException(`Class group #${classGroupId} not found`);
511
+ // completedSessions = sessions with session_date < now
512
+ const completedSessions = await this.prisma.course_class_session.count({
513
+ where: { course_class_group_id: classGroupId, session_date: { lt: now } },
514
+ });
515
+ // avgAttendance: mean of (present/total) per past session
516
+ const pastSessions = await this.prisma.course_class_session.findMany({
517
+ where: { course_class_group_id: classGroupId, session_date: { lt: now } },
518
+ select: {
519
+ id: true,
520
+ _count: { select: { course_class_attendance: true } },
521
+ course_class_attendance: {
522
+ where: { present: true },
523
+ select: { student_id: true },
524
+ },
525
+ },
526
+ });
527
+ const totalEnrolled = cg._count.course_enrollment;
528
+ let avgAttendance = 0;
529
+ if (pastSessions.length > 0 && totalEnrolled > 0) {
530
+ const pcts = pastSessions.map((s) => (s.course_class_attendance.length / totalEnrolled) * 100);
531
+ avgAttendance = this.round(pcts.reduce((a, b) => a + b, 0) / pcts.length, 0);
532
+ }
533
+ const nextSession = (_a = cg.course_class_session[0]) !== null && _a !== void 0 ? _a : null;
534
+ return {
535
+ id: cg.id,
536
+ courseName: (_c = (_b = cg.course) === null || _b === void 0 ? void 0 : _b.title) !== null && _c !== void 0 ? _c : null,
537
+ className: cg.title,
538
+ code: cg.code,
539
+ instructorName: (_f = (_e = (_d = cg.instructor) === null || _d === void 0 ? void 0 : _d.person) === null || _e === void 0 ? void 0 : _e.name) !== null && _f !== void 0 ? _f : null,
540
+ startDate: cg.start_date,
541
+ endDate: (_g = cg.end_date) !== null && _g !== void 0 ? _g : null,
542
+ status: cg.status,
543
+ deliveryMode: cg.delivery_mode,
544
+ totalSlots: cg.capacity,
545
+ enrolledCount: totalEnrolled,
546
+ totalSessions: cg._count.course_class_session,
547
+ completedSessions,
548
+ avgAttendance,
549
+ nextSession: nextSession
550
+ ? {
551
+ date: nextSession.session_date,
552
+ time: nextSession.start_time,
553
+ location: (_h = nextSession.location) !== null && _h !== void 0 ? _h : null,
554
+ meetingUrl: (_j = nextSession.meeting_url) !== null && _j !== void 0 ? _j : null,
555
+ }
556
+ : null,
557
+ };
558
+ }
559
+ async getClassGroupStudents(userId, classGroupId, params) {
560
+ await this.assertInstructorOwnsClass(userId, classGroupId);
561
+ const page = Math.max(Number(params.page) || 1, 1);
562
+ const pageSize = Math.max(Number(params.pageSize) || 20, 1);
563
+ const skip = (page - 1) * pageSize;
564
+ const where = {
565
+ course_class_group_id: classGroupId,
566
+ status: { not: 'cancelled' },
567
+ };
568
+ if (params.search) {
569
+ where.person = { name: { contains: params.search, mode: 'insensitive' } };
570
+ }
571
+ const [enrollments, total, totalSessions] = await Promise.all([
572
+ this.prisma.course_enrollment.findMany({
573
+ where,
574
+ skip,
575
+ take: pageSize,
576
+ orderBy: { person: { name: 'asc' } },
577
+ include: {
578
+ person: { select: { id: true, name: true } },
579
+ },
580
+ }),
581
+ this.prisma.course_enrollment.count({ where }),
582
+ this.prisma.course_class_session.count({ where: { course_class_group_id: classGroupId } }),
583
+ ]);
584
+ if (enrollments.length === 0) {
585
+ return { total, page, pageSize, lastPage: Math.max(1, Math.ceil(total / pageSize)), data: [] };
586
+ }
587
+ // Fetch attendance counts per student in one query
588
+ const personIds = enrollments.map((e) => e.person_id).filter(Boolean);
589
+ const attendanceRows = await this.prisma.course_class_attendance.groupBy({
590
+ by: ['student_id'],
591
+ where: { student_id: { in: personIds }, present: true },
592
+ _count: { id: true },
593
+ });
594
+ const attendanceMap = new Map(attendanceRows.map((r) => [r.student_id, r._count.id]));
595
+ const data = enrollments.map((e) => {
596
+ var _a, _b, _c, _d, _e;
597
+ const presentCount = (_b = attendanceMap.get((_a = e.person_id) !== null && _a !== void 0 ? _a : 0)) !== null && _b !== void 0 ? _b : 0;
598
+ const attendance = totalSessions > 0 ? this.round((presentCount / totalSessions) * 100, 0) : 0;
599
+ const progress = (_c = e.progress_percent) !== null && _c !== void 0 ? _c : 0;
600
+ const grade = e.final_score != null ? Number(e.final_score) : null;
601
+ let status;
602
+ if (e.status === 'paused')
603
+ status = 'inactive';
604
+ else if (attendance < 60 || progress < 50)
605
+ status = 'risk';
606
+ else
607
+ status = 'ok';
608
+ // Filter by status if requested
609
+ return { _status: status, id: e.person_id, name: (_e = (_d = e.person) === null || _d === void 0 ? void 0 : _d.name) !== null && _e !== void 0 ? _e : null, avatarUrl: null, progress, attendance, grade, status };
610
+ }).filter((s) => !params.status || s.status === params.status);
611
+ return {
612
+ total,
613
+ page,
614
+ pageSize,
615
+ lastPage: Math.max(1, Math.ceil(total / pageSize)),
616
+ data: data.map((_a) => {
617
+ var { _status: _ } = _a, rest = __rest(_a, ["_status"]);
618
+ return rest;
619
+ }),
620
+ };
621
+ }
622
+ async getClassGroupSessions(userId, classGroupId) {
623
+ await this.assertInstructorOwnsClass(userId, classGroupId);
624
+ const now = new Date();
625
+ const sessions = await this.prisma.course_class_session.findMany({
626
+ where: { course_class_group_id: classGroupId },
627
+ orderBy: { session_date: 'asc' },
628
+ select: {
629
+ id: true,
630
+ title: true,
631
+ session_date: true,
632
+ start_time: true,
633
+ end_time: true,
634
+ location: true,
635
+ meeting_url: true,
636
+ _count: { select: { course_class_attendance: true } },
637
+ course_class_attendance: {
638
+ where: { present: true },
639
+ select: { student_id: true },
640
+ },
641
+ },
642
+ });
643
+ const totalStudents = await this.prisma.course_enrollment.count({
644
+ where: { course_class_group_id: classGroupId, status: { not: 'cancelled' } },
645
+ });
646
+ let hasFoundNext = false;
647
+ return sessions.map((s) => {
648
+ var _a, _b, _c;
649
+ const isPast = s.session_date < now;
650
+ let status;
651
+ if (isPast) {
652
+ status = 'completed';
653
+ }
654
+ else if (!hasFoundNext) {
655
+ status = 'next';
656
+ hasFoundNext = true;
657
+ }
658
+ else {
659
+ status = 'upcoming';
660
+ }
661
+ return {
662
+ id: s.id,
663
+ title: s.title,
664
+ date: s.session_date,
665
+ time: s.start_time,
666
+ endTime: (_a = s.end_time) !== null && _a !== void 0 ? _a : null,
667
+ location: (_b = s.location) !== null && _b !== void 0 ? _b : null,
668
+ meetingUrl: (_c = s.meeting_url) !== null && _c !== void 0 ? _c : null,
669
+ status,
670
+ attendanceCount: isPast ? s.course_class_attendance.length : null,
671
+ totalStudents,
672
+ };
673
+ });
674
+ }
675
+ async getClassGroupAttendance(userId, classGroupId) {
676
+ await this.assertInstructorOwnsClass(userId, classGroupId);
677
+ const now = new Date();
678
+ const [sessions, enrollments] = await Promise.all([
679
+ this.prisma.course_class_session.findMany({
680
+ where: { course_class_group_id: classGroupId, session_date: { lt: now } },
681
+ orderBy: { session_date: 'asc' },
682
+ select: {
683
+ id: true,
684
+ title: true,
685
+ session_date: true,
686
+ course_class_attendance: {
687
+ select: { student_id: true, present: true },
688
+ },
689
+ },
690
+ }),
691
+ this.prisma.course_enrollment.findMany({
692
+ where: { course_class_group_id: classGroupId, status: { not: 'cancelled' } },
693
+ select: { person_id: true, person: { select: { name: true } } },
694
+ orderBy: { person: { name: 'asc' } },
695
+ }),
696
+ ]);
697
+ return {
698
+ students: enrollments.map((e) => {
699
+ var _a, _b;
700
+ return ({
701
+ studentId: e.person_id,
702
+ studentName: (_b = (_a = e.person) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : null,
703
+ });
704
+ }),
705
+ sessions: sessions.map((s) => {
706
+ const attendanceMap = new Map(s.course_class_attendance.map((a) => [a.student_id, a.present]));
707
+ return {
708
+ sessionId: s.id,
709
+ title: s.title,
710
+ date: s.session_date,
711
+ records: enrollments.map((e) => {
712
+ var _a, _b;
713
+ return ({
714
+ studentId: e.person_id,
715
+ present: (_b = attendanceMap.get((_a = e.person_id) !== null && _a !== void 0 ? _a : 0)) !== null && _b !== void 0 ? _b : null,
716
+ });
717
+ }),
718
+ };
719
+ }),
720
+ };
721
+ }
722
+ async getClassGroupMaterials(userId, classGroupId) {
723
+ await this.assertInstructorOwnsClass(userId, classGroupId);
724
+ const materials = await this.prisma.course_class_group_material.findMany({
725
+ where: { course_class_group_id: classGroupId },
726
+ orderBy: [{ sort_order: 'asc' }, { created_at: 'asc' }],
727
+ select: {
728
+ id: true,
729
+ title: true,
730
+ description: true,
731
+ material_type: true,
732
+ file_id: true,
733
+ url: true,
734
+ course_class_session_id: true,
735
+ sort_order: true,
736
+ created_at: true,
737
+ },
738
+ });
739
+ return materials.map((m) => {
740
+ var _a, _b, _c, _d, _e, _f;
741
+ return ({
742
+ id: m.id,
743
+ title: m.title,
744
+ description: (_a = m.description) !== null && _a !== void 0 ? _a : null,
745
+ materialType: m.material_type,
746
+ fileId: (_b = m.file_id) !== null && _b !== void 0 ? _b : null,
747
+ url: (_c = m.url) !== null && _c !== void 0 ? _c : null,
748
+ sessionId: (_d = m.course_class_session_id) !== null && _d !== void 0 ? _d : null,
749
+ sortOrder: m.sort_order,
750
+ createdAt: (_f = (_e = m.created_at) === null || _e === void 0 ? void 0 : _e.toISOString()) !== null && _f !== void 0 ? _f : null,
751
+ });
752
+ });
753
+ }
754
+ async getMaterialDownloadUrl(userId, classGroupId, materialId) {
755
+ await this.assertInstructorOwnsClass(userId, classGroupId);
756
+ const material = await this.prisma.course_class_group_material.findFirst({
757
+ where: { id: materialId, course_class_group_id: classGroupId },
758
+ select: { file_id: true },
759
+ });
760
+ if (!material) {
761
+ throw new common_1.NotFoundException('Material not found');
762
+ }
763
+ if (!material.file_id) {
764
+ throw new common_1.NotFoundException('This material has no associated file');
765
+ }
766
+ const file = await this.fileService.get(material.file_id);
767
+ return { url: await this.fileService.tempURL(file.path) };
768
+ }
769
+ async uploadMaterial(userId, classGroupId, file) {
770
+ var _a, _b, _c, _d, _e, _f;
771
+ await this.assertInstructorOwnsClass(userId, classGroupId);
772
+ const uploaded = await this.fileService.upload('class-materials', file);
773
+ const material = await this.prisma.course_class_group_material.create({
774
+ data: {
775
+ course_class_group_id: classGroupId,
776
+ title: uploaded.filename,
777
+ material_type: 'file',
778
+ file_id: uploaded.id,
779
+ sort_order: 0,
780
+ },
781
+ select: {
782
+ id: true,
783
+ title: true,
784
+ description: true,
785
+ material_type: true,
786
+ file_id: true,
787
+ url: true,
788
+ course_class_session_id: true,
789
+ sort_order: true,
790
+ created_at: true,
791
+ },
792
+ });
793
+ return {
794
+ id: material.id,
795
+ title: material.title,
796
+ description: (_a = material.description) !== null && _a !== void 0 ? _a : null,
797
+ materialType: material.material_type,
798
+ fileId: (_b = material.file_id) !== null && _b !== void 0 ? _b : null,
799
+ url: (_c = material.url) !== null && _c !== void 0 ? _c : null,
800
+ sessionId: (_d = material.course_class_session_id) !== null && _d !== void 0 ? _d : null,
801
+ sortOrder: material.sort_order,
802
+ createdAt: (_f = (_e = material.created_at) === null || _e === void 0 ? void 0 : _e.toISOString()) !== null && _f !== void 0 ? _f : null,
803
+ };
804
+ }
805
+ async deleteMaterial(userId, classGroupId, materialId) {
806
+ await this.assertInstructorOwnsClass(userId, classGroupId);
807
+ const material = await this.prisma.course_class_group_material.findFirst({
808
+ where: { id: materialId, course_class_group_id: classGroupId },
809
+ select: { id: true },
810
+ });
811
+ if (!material) {
812
+ throw new common_1.NotFoundException('Material not found');
813
+ }
814
+ await this.prisma.course_class_group_material.delete({
815
+ where: { id: materialId },
816
+ });
817
+ return { success: true };
818
+ }
819
+ async getClassEvaluations(userId, classGroupId) {
820
+ await this.assertInstructorOwnsClass(userId, classGroupId);
821
+ // Fetch sessions of this class group
822
+ const sessions = await this.prisma.course_class_session.findMany({
823
+ where: { course_class_group_id: classGroupId },
824
+ select: { id: true, title: true, session_date: true },
825
+ orderBy: { session_date: 'asc' },
826
+ });
827
+ const sessionIds = sessions.map((s) => s.id);
828
+ if (sessionIds.length === 0) {
829
+ return {
830
+ totalRatings: 0,
831
+ averageScore: 0,
832
+ distribution: [
833
+ { range: '9–10', count: 0 },
834
+ { range: '7–8', count: 0 },
835
+ { range: '5–6', count: 0 },
836
+ { range: '0–4', count: 0 },
837
+ ],
838
+ sessions: [],
839
+ };
840
+ }
841
+ // Fetch active evaluation topics for these sessions (anonymous — scores + comments, per-topic names)
842
+ const topics = await this.prisma.evaluation_topic.findMany({
843
+ where: {
844
+ course_class_session_id: { in: sessionIds },
845
+ target_type: 'course_class_session',
846
+ is_active: true,
847
+ },
848
+ select: {
849
+ id: true,
850
+ name: true,
851
+ course_class_session_id: true,
852
+ evaluation_rating: { select: { score: true, comment: true } },
853
+ },
854
+ });
855
+ const sessionMap = new Map();
856
+ for (const sessId of sessionIds) {
857
+ sessionMap.set(sessId, { scores: [], comments: [], topics: new Map() });
858
+ }
859
+ for (const topic of topics) {
860
+ const sessId = topic.course_class_session_id;
861
+ if (sessId == null)
862
+ continue;
863
+ const agg = sessionMap.get(sessId);
864
+ if (!agg)
865
+ continue;
866
+ if (!agg.topics.has(topic.id)) {
867
+ agg.topics.set(topic.id, { name: topic.name, scores: [] });
868
+ }
869
+ const topicAgg = agg.topics.get(topic.id);
870
+ for (const r of topic.evaluation_rating) {
871
+ const score = Number(r.score);
872
+ agg.scores.push(score);
873
+ topicAgg.scores.push(score);
874
+ if (r.comment)
875
+ agg.comments.push(r.comment);
876
+ }
877
+ }
878
+ const allScores = [...sessionMap.values()].flatMap((a) => a.scores);
879
+ const totalRatings = allScores.length;
880
+ const averageScore = totalRatings > 0 ? this.round(allScores.reduce((a, b) => a + b, 0) / totalRatings, 1) : 0;
881
+ const distribution = [
882
+ { range: '9–10', count: allScores.filter((s) => s >= 9).length },
883
+ { range: '7–8', count: allScores.filter((s) => s >= 7 && s < 9).length },
884
+ { range: '5–6', count: allScores.filter((s) => s >= 5 && s < 7).length },
885
+ { range: '0–4', count: allScores.filter((s) => s < 5).length },
886
+ ];
887
+ const sessionData = sessions.map((s) => {
888
+ var _a;
889
+ const agg = (_a = sessionMap.get(s.id)) !== null && _a !== void 0 ? _a : {
890
+ scores: [], comments: [], topics: new Map(),
891
+ };
892
+ const avg = agg.scores.length > 0
893
+ ? this.round(agg.scores.reduce((a, b) => a + b, 0) / agg.scores.length, 1)
894
+ : null;
895
+ const topicBreakdown = Array.from(agg.topics.entries()).map(([topicId, { name, scores }]) => ({
896
+ topicId,
897
+ name,
898
+ avgScore: scores.length > 0
899
+ ? this.round(scores.reduce((a, b) => a + b, 0) / scores.length, 1)
900
+ : null,
901
+ }));
902
+ return {
903
+ sessionId: s.id,
904
+ title: s.title,
905
+ date: s.session_date,
906
+ avgScore: avg,
907
+ ratingCount: agg.scores.length,
908
+ topics: topicBreakdown,
909
+ comments: agg.comments,
910
+ };
911
+ });
912
+ return { totalRatings, averageScore, distribution, sessions: sessionData };
913
+ }
914
+ async getReports(userId, enterpriseId, dateFrom, dateTo) {
915
+ const instructor = await this.prisma.instructor.findFirst({
916
+ where: { person: { person_user: { some: { user_id: userId } } } },
917
+ select: { id: true },
918
+ });
919
+ if (!instructor)
920
+ throw new common_1.NotFoundException('No instructor profile found for this user');
921
+ const now = new Date();
922
+ // Date range for period filtering
923
+ const startDate = dateFrom ? new Date(dateFrom) : new Date(now.getFullYear(), 0, 1);
924
+ const endDate = dateTo ? new Date(dateTo + 'T23:59:59.999Z') : new Date(now.getFullYear(), 11, 31, 23, 59, 59);
925
+ const sessionDateFilter = { gte: startDate, lte: endDate };
926
+ // Treat enterpriseId=0 as "no filter" (frontend may send 0 when no enterprise is selected)
927
+ const effectiveEnterpriseId = enterpriseId && enterpriseId > 0 ? enterpriseId : undefined;
928
+ const enterpriseFilter = effectiveEnterpriseId !== undefined
929
+ ? { enterprise_class_group: { some: { enterprise_id: effectiveEnterpriseId } } }
930
+ : {};
931
+ // ── 1. Collect all class groups taught by this instructor ──────────────────
932
+ const sessionRows = await this.prisma.course_class_session.findMany({
933
+ where: Object.assign({ course_class_session_instructor: { some: { instructor_id: instructor.id } }, session_date: sessionDateFilter }, (effectiveEnterpriseId !== undefined ? { course_class_group: enterpriseFilter } : {})),
934
+ select: { course_class_group_id: true },
935
+ distinct: ['course_class_group_id'],
936
+ });
937
+ const cgIds = sessionRows
938
+ .map((r) => r.course_class_group_id)
939
+ .filter((id) => id != null);
940
+ const CLASS_COLORS = ['#F97316', '#6366F1', '#10B981', '#0EA5E9', '#8B5CF6', '#F43F5E'];
941
+ if (cgIds.length === 0) {
942
+ return {
943
+ kpis: { totalStudents: 0, averageEvaluationScore: 0, averageAttendance: 0, sessionsCompleted: 0 },
944
+ classes: [],
945
+ topicNames: [],
946
+ evaluationsBySession: [],
947
+ attendanceBySession: [],
948
+ studentGrades: [],
949
+ };
950
+ }
951
+ const classGroupRows = await this.prisma.course_class_group.findMany({
952
+ where: { id: { in: cgIds } },
953
+ orderBy: { start_date: 'asc' },
954
+ select: { id: true, title: true },
955
+ take: 6,
956
+ });
957
+ const cgIdsCapped = classGroupRows.map((cg) => cg.id);
958
+ const classes = classGroupRows.map((cg, i) => {
959
+ var _a;
960
+ return ({
961
+ key: `t${i + 1}`,
962
+ name: cg.title,
963
+ color: (_a = CLASS_COLORS[i]) !== null && _a !== void 0 ? _a : '#94A3B8',
964
+ });
965
+ });
966
+ const classKeyById = new Map(classGroupRows.map((cg, i) => [cg.id, `t${i + 1}`]));
967
+ const classNameById = new Map(classGroupRows.map((cg) => [cg.id, cg.title]));
968
+ // ── 2. Parallel data fetches ───────────────────────────────────────────────
969
+ const [allSessions, evalTopics, attendanceRows, enrollmentRows, completedSessionCount] = await Promise.all([
970
+ // All sessions for these class groups within the date range
971
+ this.prisma.course_class_session.findMany({
972
+ where: { course_class_group_id: { in: cgIdsCapped }, session_date: sessionDateFilter },
973
+ select: {
974
+ id: true,
975
+ title: true,
976
+ session_date: true,
977
+ course_class_group_id: true,
978
+ },
979
+ orderBy: { session_date: 'asc' },
980
+ }),
981
+ // Evaluation topics+ratings for sessions taught by this instructor within the date range
982
+ this.prisma.evaluation_topic.findMany({
983
+ where: {
984
+ target_type: 'course_class_session',
985
+ is_active: true,
986
+ course_class_session: {
987
+ course_class_group_id: { in: cgIdsCapped },
988
+ session_date: sessionDateFilter,
989
+ },
990
+ },
991
+ select: {
992
+ id: true,
993
+ name: true,
994
+ course_class_session_id: true,
995
+ evaluation_rating: { select: { score: true } },
996
+ },
997
+ }),
998
+ // Attendance records for sessions in the date range
999
+ this.prisma.course_class_attendance.findMany({
1000
+ where: {
1001
+ course_class_session: {
1002
+ course_class_group_id: { in: cgIdsCapped },
1003
+ session_date: { gte: startDate, lt: now < endDate ? now : endDate },
1004
+ },
1005
+ },
1006
+ select: {
1007
+ course_class_session_id: true,
1008
+ student_id: true,
1009
+ present: true,
1010
+ },
1011
+ }),
1012
+ // Enrollment final scores (notas de prova)
1013
+ this.prisma.course_enrollment.findMany({
1014
+ where: {
1015
+ course_class_group_id: { in: cgIdsCapped },
1016
+ final_score: { not: null },
1017
+ status: { not: 'cancelled' },
1018
+ },
1019
+ select: {
1020
+ course_class_group_id: true,
1021
+ final_score: true,
1022
+ person: { select: { name: true } },
1023
+ },
1024
+ orderBy: { final_score: 'desc' },
1025
+ }),
1026
+ // Completed sessions count within the date range
1027
+ this.prisma.course_class_session.count({
1028
+ where: {
1029
+ course_class_group_id: { in: cgIdsCapped },
1030
+ session_date: { gte: startDate, lt: now < endDate ? now : endDate },
1031
+ },
1032
+ }),
1033
+ ]);
1034
+ // ── 3. KPIs ────────────────────────────────────────────────────────────────
1035
+ const totalStudents = await this.prisma.course_enrollment.count({
1036
+ where: {
1037
+ course_class_group_id: { in: cgIdsCapped },
1038
+ status: { not: 'cancelled' },
1039
+ },
1040
+ });
1041
+ // Average evaluation score: compute from already-filtered evalTopics
1042
+ const allEvalScores = [];
1043
+ for (const topic of evalTopics) {
1044
+ for (const r of topic.evaluation_rating)
1045
+ allEvalScores.push(Number(r.score));
1046
+ }
1047
+ const averageEvaluationScore = allEvalScores.length > 0
1048
+ ? this.round(allEvalScores.reduce((a, b) => a + b, 0) / allEvalScores.length, 2)
1049
+ : 0;
1050
+ // Average attendance: per session, count present / total records
1051
+ const pastSessions = allSessions.filter((s) => s.session_date < now);
1052
+ let attendanceSum = 0;
1053
+ let attendanceDenom = 0;
1054
+ for (const sess of pastSessions) {
1055
+ const records = attendanceRows.filter((r) => r.course_class_session_id === sess.id);
1056
+ if (records.length > 0) {
1057
+ attendanceSum += records.filter((r) => r.present).length;
1058
+ attendanceDenom += records.length;
1059
+ }
1060
+ }
1061
+ const averageAttendance = attendanceDenom > 0 ? this.round((attendanceSum / attendanceDenom) * 100, 0) : 0;
1062
+ const sessEvalMap = new Map();
1063
+ for (const s of allSessions)
1064
+ sessEvalMap.set(s.id, { topics: new Map() });
1065
+ // Collect all unique topic names
1066
+ const allTopicNames = new Set();
1067
+ for (const topic of evalTopics) {
1068
+ if (topic.course_class_session_id == null)
1069
+ continue;
1070
+ allTopicNames.add(topic.name);
1071
+ const agg = sessEvalMap.get(topic.course_class_session_id);
1072
+ if (!agg)
1073
+ continue;
1074
+ if (!agg.topics.has(topic.name))
1075
+ agg.topics.set(topic.name, []);
1076
+ for (const r of topic.evaluation_rating) {
1077
+ agg.topics.get(topic.name).push(Number(r.score));
1078
+ }
1079
+ }
1080
+ const topicNames = Array.from(allTopicNames).sort();
1081
+ const evaluationsBySession = allSessions
1082
+ .filter((s) => {
1083
+ const agg = sessEvalMap.get(s.id);
1084
+ return agg && [...agg.topics.values()].some((scores) => scores.length > 0);
1085
+ })
1086
+ .map((s) => {
1087
+ var _a, _b, _c, _d, _e;
1088
+ const agg = sessEvalMap.get(s.id);
1089
+ const topicsObj = {};
1090
+ let totalScore = 0;
1091
+ let totalCount = 0;
1092
+ for (const tName of topicNames) {
1093
+ const scores = (_a = agg.topics.get(tName)) !== null && _a !== void 0 ? _a : [];
1094
+ if (scores.length > 0) {
1095
+ const avg = this.round(scores.reduce((a, b) => a + b, 0) / scores.length, 2);
1096
+ topicsObj[tName] = avg;
1097
+ totalScore += avg;
1098
+ totalCount++;
1099
+ }
1100
+ else {
1101
+ topicsObj[tName] = null;
1102
+ }
1103
+ }
1104
+ const media = totalCount > 0 ? this.round(totalScore / totalCount, 2) : 0;
1105
+ const classKey = (_c = classKeyById.get((_b = s.course_class_group_id) !== null && _b !== void 0 ? _b : -1)) !== null && _c !== void 0 ? _c : null;
1106
+ const className = (_e = classNameById.get((_d = s.course_class_group_id) !== null && _d !== void 0 ? _d : -1)) !== null && _e !== void 0 ? _e : null;
1107
+ // Short label: first 20 chars of title
1108
+ const label = s.title.length > 20 ? `${s.title.slice(0, 18)}…` : s.title;
1109
+ return {
1110
+ sessionId: s.id,
1111
+ label,
1112
+ date: s.session_date,
1113
+ classKey,
1114
+ className,
1115
+ topics: topicsObj,
1116
+ media,
1117
+ hi: Math.min(5, this.round(media + 0.5, 2)),
1118
+ lo: Math.max(0, this.round(media - 0.5, 2)),
1119
+ };
1120
+ });
1121
+ // ── 5. Attendance by session ───────────────────────────────────────────────
1122
+ const attendanceBySession = pastSessions
1123
+ .filter((s) => attendanceRows.some((r) => r.course_class_session_id === s.id))
1124
+ .map((s) => {
1125
+ var _a, _b, _c, _d;
1126
+ const records = attendanceRows.filter((r) => r.course_class_session_id === s.id);
1127
+ const presentCount = records.filter((r) => r.present).length;
1128
+ const classKey = (_b = classKeyById.get((_a = s.course_class_group_id) !== null && _a !== void 0 ? _a : -1)) !== null && _b !== void 0 ? _b : null;
1129
+ const className = (_d = classNameById.get((_c = s.course_class_group_id) !== null && _c !== void 0 ? _c : -1)) !== null && _d !== void 0 ? _d : null;
1130
+ const label = s.title.length > 12 ? `${s.title.slice(0, 10)}…` : s.title;
1131
+ return {
1132
+ sessionId: s.id,
1133
+ label,
1134
+ date: s.session_date,
1135
+ classKey,
1136
+ className,
1137
+ presentCount,
1138
+ };
1139
+ });
1140
+ // ── 6. Student grades ──────────────────────────────────────────────────────
1141
+ const studentGrades = enrollmentRows
1142
+ .filter((e) => e.final_score != null)
1143
+ .map((e) => {
1144
+ var _a, _b, _c, _d, _e, _f;
1145
+ return ({
1146
+ name: (_b = (_a = e.person) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : 'Aluno',
1147
+ grade: Number(e.final_score),
1148
+ classKey: (_d = classKeyById.get((_c = e.course_class_group_id) !== null && _c !== void 0 ? _c : -1)) !== null && _d !== void 0 ? _d : null,
1149
+ className: (_f = classNameById.get((_e = e.course_class_group_id) !== null && _e !== void 0 ? _e : -1)) !== null && _f !== void 0 ? _f : null,
1150
+ });
1151
+ })
1152
+ .sort((a, b) => b.grade - a.grade);
1153
+ return {
1154
+ kpis: {
1155
+ totalStudents,
1156
+ averageEvaluationScore,
1157
+ averageAttendance,
1158
+ sessionsCompleted: completedSessionCount,
1159
+ },
1160
+ classes,
1161
+ topicNames,
1162
+ evaluationsBySession,
1163
+ attendanceBySession,
1164
+ studentGrades,
1165
+ };
1166
+ }
1167
+ round(value, digits = 0) {
1168
+ const factor = 10 ** digits;
1169
+ return Math.round(value * factor) / factor;
1170
+ }
1171
+ addDays(date, days) {
1172
+ const result = new Date(date);
1173
+ result.setDate(result.getDate() + days);
1174
+ return result;
1175
+ }
1176
+ async submitSessionAttendance(userId, classGroupId, sessionId, records) {
1177
+ await this.assertInstructorOwnsClass(userId, classGroupId);
1178
+ const session = await this.prisma.course_class_session.findFirst({
1179
+ where: { id: sessionId, course_class_group_id: classGroupId },
1180
+ select: { id: true },
1181
+ });
1182
+ if (!session)
1183
+ throw new common_1.NotFoundException('Session not found in this class group');
1184
+ await this.prisma.$transaction([
1185
+ this.prisma.course_class_attendance.deleteMany({
1186
+ where: { course_class_session_id: sessionId },
1187
+ }),
1188
+ this.prisma.course_class_attendance.createMany({
1189
+ data: records.map((r) => ({
1190
+ course_class_session_id: sessionId,
1191
+ student_id: r.studentId,
1192
+ present: r.present,
1193
+ })),
1194
+ }),
1195
+ ]);
1196
+ return { success: true, count: records.length };
1197
+ }
1198
+ endOfDay(date) {
1199
+ const result = new Date(date);
1200
+ result.setHours(23, 59, 59, 999);
1201
+ return result;
1202
+ }
1203
+ startOfWeek(date) {
1204
+ const result = new Date(date);
1205
+ result.setHours(0, 0, 0, 0);
1206
+ const day = result.getDay();
1207
+ const diff = day === 0 ? -6 : 1 - day;
1208
+ result.setDate(result.getDate() + diff);
1209
+ return result;
1210
+ }
1211
+ };
1212
+ exports.TrainingInstructorService = TrainingInstructorService;
1213
+ exports.TrainingInstructorService = TrainingInstructorService = __decorate([
1214
+ (0, common_1.Injectable)(),
1215
+ __metadata("design:paramtypes", [api_prisma_1.PrismaService,
1216
+ core_1.FileService])
1217
+ ], TrainingInstructorService);
1218
+ //# sourceMappingURL=training-instructor.service.js.map