@hed-hog/lms 0.0.318 → 0.0.320

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 +65 -2
  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 +67 -2
  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 +27 -47
  91. package/hedhog/frontend/app/classes/[id]/_components/event-summary-popover.tsx.ejs +15 -15
  92. package/hedhog/frontend/app/classes/[id]/_components/quick-create-session-popover.tsx.ejs +5 -5
  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 +21 -8
  96. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +10 -6
  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 +621 -1250
  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 +5 -5
  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 +8 -8
  109. package/src/class-group/dto/material.dto.ts +69 -0
  110. package/src/course/course.service.ts +54 -21
  111. package/src/course/dto/create-course.dto.ts +8 -8
  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,674 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.TrainingStudentService = void 0;
13
+ const api_prisma_1 = require("@hed-hog/api-prisma");
14
+ const common_1 = require("@nestjs/common");
15
+ let TrainingStudentService = class TrainingStudentService {
16
+ constructor(prisma) {
17
+ this.prisma = prisma;
18
+ }
19
+ async getDashboard(userId, enterpriseId) {
20
+ var _a;
21
+ const personUser = await this.prisma.person_user.findFirst({
22
+ where: { user_id: userId },
23
+ select: { person_id: true },
24
+ });
25
+ if (!personUser) {
26
+ throw new common_1.NotFoundException('No person profile found for this user');
27
+ }
28
+ const personId = personUser.person_id;
29
+ const now = new Date();
30
+ const weekStart = this.startOfWeek(now);
31
+ const weekEnd = this.endOfDay(this.addDays(weekStart, 6));
32
+ // Fetch all active enrollments for this student
33
+ const enrollments = await this.prisma.course_enrollment.findMany({
34
+ where: { person_id: personId, status: { not: 'cancelled' } },
35
+ select: {
36
+ id: true,
37
+ course_id: true,
38
+ course_class_group_id: true,
39
+ status: true,
40
+ progress_percent: true,
41
+ final_score: true,
42
+ enrolled_at: true,
43
+ completed_at: true,
44
+ },
45
+ });
46
+ let classEnrollments = enrollments.filter((e) => e.course_class_group_id != null);
47
+ // If enterpriseId provided, restrict to class groups linked to that enterprise
48
+ if (enterpriseId !== undefined) {
49
+ const enterpriseCgRows = await this.prisma.enterprise_class_group.findMany({
50
+ where: { enterprise_id: enterpriseId },
51
+ select: { course_class_group_id: true },
52
+ });
53
+ const enterpriseCgIds = new Set(enterpriseCgRows.map((r) => r.course_class_group_id));
54
+ classEnrollments = classEnrollments.filter((e) => enterpriseCgIds.has(e.course_class_group_id));
55
+ }
56
+ const onDemandEnrollments = enrollments.filter((e) => e.course_class_group_id == null);
57
+ const cgIds = classEnrollments
58
+ .map((e) => e.course_class_group_id)
59
+ .filter((id) => id != null);
60
+ const enrollmentIds = enrollments.map((e) => e.id);
61
+ const [kpis, myClasses, onDemandCourses, weeklySchedule, ranking, gamification] = await Promise.all([
62
+ this.getKpis(personId, enrollments, enrollmentIds),
63
+ this.getMyClasses(classEnrollments, now),
64
+ this.getOnDemandCourses(onDemandEnrollments),
65
+ this.getWeeklySchedule(cgIds, weekStart, weekEnd),
66
+ this.getRanking(personId, cgIds),
67
+ this.getGamification(personId, enrollmentIds, weekStart, weekEnd),
68
+ ]);
69
+ const myRanking = ranking.find((r) => r.isMe);
70
+ return {
71
+ kpis,
72
+ myClasses,
73
+ onDemandCourses,
74
+ weeklySchedule,
75
+ ranking,
76
+ gamification: Object.assign(Object.assign({}, gamification), { rankingPosition: (_a = myRanking === null || myRanking === void 0 ? void 0 : myRanking.position) !== null && _a !== void 0 ? _a : null }),
77
+ };
78
+ }
79
+ async getClassGroups(userId, options = {}) {
80
+ const personUser = await this.prisma.person_user.findFirst({
81
+ where: { user_id: userId },
82
+ select: { person_id: true },
83
+ });
84
+ if (!personUser) {
85
+ throw new common_1.NotFoundException('No person profile found for this user');
86
+ }
87
+ const personId = personUser.person_id;
88
+ const { enterpriseId, search, status, deliveryMode } = options;
89
+ const now = new Date();
90
+ let cgFilter = {};
91
+ if (enterpriseId !== undefined) {
92
+ cgFilter = { enterprise_class_group: { some: { enterprise_id: enterpriseId } } };
93
+ }
94
+ if (status) {
95
+ cgFilter = Object.assign(Object.assign({}, cgFilter), { status: status });
96
+ }
97
+ if (deliveryMode) {
98
+ cgFilter = Object.assign(Object.assign({}, cgFilter), { delivery_mode: deliveryMode });
99
+ }
100
+ if (search) {
101
+ cgFilter = Object.assign(Object.assign({}, cgFilter), { OR: [
102
+ { title: { contains: search, mode: 'insensitive' } },
103
+ { code: { contains: search, mode: 'insensitive' } },
104
+ { course: { title: { contains: search, mode: 'insensitive' } } },
105
+ ] });
106
+ }
107
+ const enrollments = await this.prisma.course_enrollment.findMany({
108
+ where: {
109
+ person_id: personId,
110
+ status: { not: 'cancelled' },
111
+ course_class_group_id: { not: null },
112
+ course_class_group: Object.keys(cgFilter).length > 0 ? cgFilter : undefined,
113
+ },
114
+ include: {
115
+ course_class_group: {
116
+ include: {
117
+ course: {
118
+ select: {
119
+ id: true,
120
+ title: true,
121
+ course_image: {
122
+ where: { image_type: { slug: { in: ['course-logo', 'course-banner'] } } },
123
+ orderBy: { is_primary: 'desc' },
124
+ select: {
125
+ file: { select: { location: true } },
126
+ image_type: { select: { slug: true } },
127
+ },
128
+ },
129
+ },
130
+ },
131
+ instructor: {
132
+ include: { person: { select: { name: true } } },
133
+ },
134
+ course_class_session: {
135
+ where: { session_date: { gte: now } },
136
+ orderBy: { session_date: 'asc' },
137
+ take: 1,
138
+ select: { session_date: true, start_time: true, location: true },
139
+ },
140
+ },
141
+ },
142
+ },
143
+ orderBy: { enrolled_at: 'desc' },
144
+ });
145
+ // Compute session totals per class group for hour estimation
146
+ const cgIds = enrollments
147
+ .map((e) => e.course_class_group_id)
148
+ .filter((id) => id != null);
149
+ const [totalSessionCounts, pastSessionCounts] = cgIds.length > 0
150
+ ? await Promise.all([
151
+ this.prisma.course_class_session.groupBy({
152
+ by: ['course_class_group_id'],
153
+ where: { course_class_group_id: { in: cgIds } },
154
+ _count: { id: true },
155
+ }),
156
+ this.prisma.course_class_session.groupBy({
157
+ by: ['course_class_group_id'],
158
+ where: { course_class_group_id: { in: cgIds }, session_date: { lt: now } },
159
+ _count: { id: true },
160
+ }),
161
+ ])
162
+ : [[], []];
163
+ const totalSessionMap = new Map(totalSessionCounts.map((r) => [r.course_class_group_id, r._count.id]));
164
+ const pastSessionMap = new Map(pastSessionCounts.map((r) => [r.course_class_group_id, r._count.id]));
165
+ const HOURS_PER_SESSION = 2;
166
+ const data = enrollments.map((enrollment) => {
167
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
168
+ const cg = enrollment.course_class_group;
169
+ if (!cg)
170
+ return null;
171
+ const nextSession = (_a = cg.course_class_session[0]) !== null && _a !== void 0 ? _a : null;
172
+ const totalSessions = (_b = totalSessionMap.get(cg.id)) !== null && _b !== void 0 ? _b : 0;
173
+ const pastSessions = (_c = pastSessionMap.get(cg.id)) !== null && _c !== void 0 ? _c : 0;
174
+ return {
175
+ id: enrollment.id,
176
+ classGroupId: cg.id,
177
+ courseId: (_e = (_d = cg.course) === null || _d === void 0 ? void 0 : _d.id) !== null && _e !== void 0 ? _e : null,
178
+ courseName: (_g = (_f = cg.course) === null || _f === void 0 ? void 0 : _f.title) !== null && _g !== void 0 ? _g : null,
179
+ courseLogoUrl: (_l = (_k = (_j = (_h = cg.course) === null || _h === void 0 ? void 0 : _h.course_image.find((ci) => { var _a; return ((_a = ci.image_type) === null || _a === void 0 ? void 0 : _a.slug) === 'course-logo'; })) === null || _j === void 0 ? void 0 : _j.file) === null || _k === void 0 ? void 0 : _k.location) !== null && _l !== void 0 ? _l : null,
180
+ courseBannerUrl: (_q = (_p = (_o = (_m = cg.course) === null || _m === void 0 ? void 0 : _m.course_image.find((ci) => { var _a; return ((_a = ci.image_type) === null || _a === void 0 ? void 0 : _a.slug) === 'course-banner'; })) === null || _o === void 0 ? void 0 : _o.file) === null || _p === void 0 ? void 0 : _p.location) !== null && _q !== void 0 ? _q : null,
181
+ className: cg.title,
182
+ code: (_r = cg.code) !== null && _r !== void 0 ? _r : '',
183
+ instructorName: (_u = (_t = (_s = cg.instructor) === null || _s === void 0 ? void 0 : _s.person) === null || _t === void 0 ? void 0 : _t.name) !== null && _u !== void 0 ? _u : null,
184
+ instructorAvatarUrl: null,
185
+ progress: (_v = enrollment.progress_percent) !== null && _v !== void 0 ? _v : 0,
186
+ nextSession: nextSession
187
+ ? {
188
+ date: nextSession.session_date,
189
+ time: nextSession.start_time,
190
+ location: (_w = nextSession.location) !== null && _w !== void 0 ? _w : null,
191
+ }
192
+ : null,
193
+ status: cg.status,
194
+ deliveryMode: (_x = cg.delivery_mode) !== null && _x !== void 0 ? _x : null,
195
+ totalHours: totalSessions * HOURS_PER_SESSION,
196
+ completedHours: pastSessions * HOURS_PER_SESSION,
197
+ };
198
+ }).filter((item) => item !== null);
199
+ return { data, total: data.length };
200
+ }
201
+ // ── Private helpers ───────────────────────────────────────────────────────────
202
+ async getKpis(personId, enrollments, enrollmentIds) {
203
+ const completedCourses = enrollments.filter((e) => e.status === 'completed').length;
204
+ const activeEnrollments = enrollments.filter((e) => ['active', 'completed'].includes(e.status));
205
+ const overallProgress = activeEnrollments.length > 0
206
+ ? this.round(activeEnrollments.reduce((sum, e) => { var _a; return sum + ((_a = e.progress_percent) !== null && _a !== void 0 ? _a : 0); }, 0) /
207
+ activeEnrollments.length, 0)
208
+ : 0;
209
+ const [pendingEvaluations, attendanceData] = await Promise.all([
210
+ enrollmentIds.length > 0
211
+ ? this.prisma.exam_attempt.count({
212
+ where: {
213
+ course_enrollment_id: { in: enrollmentIds },
214
+ status: { not: 'completed' },
215
+ },
216
+ })
217
+ : Promise.resolve(0),
218
+ this.prisma.course_class_attendance.findMany({
219
+ where: { student_id: personId },
220
+ select: { present: true },
221
+ }),
222
+ ]);
223
+ const totalAttendance = attendanceData.length;
224
+ const presentAttendance = attendanceData.filter((a) => a.present).length;
225
+ const attendancePct = totalAttendance > 0 ? this.round((presentAttendance / totalAttendance) * 100, 0) : 0;
226
+ return {
227
+ attendancePct: {
228
+ value: attendancePct,
229
+ change: null,
230
+ changeType: 'percent',
231
+ },
232
+ overallProgress: {
233
+ value: overallProgress,
234
+ change: null,
235
+ changeType: 'percent',
236
+ },
237
+ completedCourses: {
238
+ value: completedCourses,
239
+ change: null,
240
+ changeType: 'absolute',
241
+ },
242
+ pendingEvaluations: {
243
+ value: pendingEvaluations,
244
+ change: null,
245
+ changeType: 'absolute',
246
+ },
247
+ };
248
+ }
249
+ async getMyClasses(classEnrollments, now) {
250
+ if (classEnrollments.length === 0)
251
+ return [];
252
+ const cgIds = classEnrollments
253
+ .map((e) => e.course_class_group_id)
254
+ .filter((id) => id != null);
255
+ const classGroups = await this.prisma.course_class_group.findMany({
256
+ where: { id: { in: cgIds } },
257
+ include: {
258
+ course: {
259
+ select: {
260
+ id: true,
261
+ title: true,
262
+ course_image: {
263
+ where: { image_type: { slug: 'course-logo' } },
264
+ orderBy: { is_primary: 'desc' },
265
+ take: 1,
266
+ select: { file: { select: { id: true } } },
267
+ },
268
+ },
269
+ },
270
+ course_class_session: {
271
+ where: { session_date: { gte: now } },
272
+ orderBy: { session_date: 'asc' },
273
+ take: 1,
274
+ include: {
275
+ course_class_session_instructor: {
276
+ where: { role: 'lead' },
277
+ take: 1,
278
+ include: {
279
+ instructor: { include: { person: { select: { name: true } } } },
280
+ },
281
+ },
282
+ },
283
+ },
284
+ },
285
+ });
286
+ const cgById = new Map(classGroups.map((cg) => [cg.id, cg]));
287
+ return classEnrollments.map((enrollment) => {
288
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
289
+ const cg = cgById.get(enrollment.course_class_group_id);
290
+ const nextSession = (_a = cg === null || cg === void 0 ? void 0 : cg.course_class_session[0]) !== null && _a !== void 0 ? _a : null;
291
+ const leadInstructor = (_e = (_d = (_c = (_b = nextSession === null || nextSession === void 0 ? void 0 : nextSession.course_class_session_instructor[0]) === null || _b === void 0 ? void 0 : _b.instructor) === null || _c === void 0 ? void 0 : _c.person) === null || _d === void 0 ? void 0 : _d.name) !== null && _e !== void 0 ? _e : null;
292
+ return {
293
+ enrollmentId: enrollment.id,
294
+ classGroupId: enrollment.course_class_group_id,
295
+ courseId: (_g = (_f = cg === null || cg === void 0 ? void 0 : cg.course) === null || _f === void 0 ? void 0 : _f.id) !== null && _g !== void 0 ? _g : null,
296
+ courseName: (_j = (_h = cg === null || cg === void 0 ? void 0 : cg.course) === null || _h === void 0 ? void 0 : _h.title) !== null && _j !== void 0 ? _j : null,
297
+ hasCourseLogo: !!((_l = (_k = cg === null || cg === void 0 ? void 0 : cg.course) === null || _k === void 0 ? void 0 : _k.course_image) === null || _l === void 0 ? void 0 : _l[0]),
298
+ className: (_m = cg === null || cg === void 0 ? void 0 : cg.title) !== null && _m !== void 0 ? _m : null,
299
+ instructorName: leadInstructor,
300
+ progress: (_o = enrollment.progress_percent) !== null && _o !== void 0 ? _o : 0,
301
+ nextSession: nextSession
302
+ ? {
303
+ date: nextSession.session_date,
304
+ time: nextSession.start_time,
305
+ location: (_p = nextSession.location) !== null && _p !== void 0 ? _p : null,
306
+ }
307
+ : null,
308
+ status: (_q = cg === null || cg === void 0 ? void 0 : cg.status) !== null && _q !== void 0 ? _q : null,
309
+ deliveryMode: (_r = cg === null || cg === void 0 ? void 0 : cg.delivery_mode) !== null && _r !== void 0 ? _r : null,
310
+ };
311
+ });
312
+ }
313
+ async getOnDemandCourses(onDemandEnrollments) {
314
+ if (onDemandEnrollments.length === 0)
315
+ return [];
316
+ const courseIds = onDemandEnrollments.map((e) => e.course_id);
317
+ const courses = await this.prisma.course.findMany({
318
+ where: { id: { in: courseIds } },
319
+ select: {
320
+ id: true,
321
+ title: true,
322
+ duration_hours: true,
323
+ course_image: {
324
+ where: { image_type: { slug: 'course-logo' } },
325
+ orderBy: { is_primary: 'desc' },
326
+ take: 1,
327
+ select: { file: { select: { id: true } } },
328
+ },
329
+ },
330
+ });
331
+ const courseById = new Map(courses.map((c) => [c.id, c]));
332
+ return onDemandEnrollments.map((enrollment) => {
333
+ var _a, _b, _c, _d;
334
+ const course = courseById.get(enrollment.course_id);
335
+ return {
336
+ enrollmentId: enrollment.id,
337
+ courseId: enrollment.course_id,
338
+ courseName: (_a = course === null || course === void 0 ? void 0 : course.title) !== null && _a !== void 0 ? _a : null,
339
+ hasCourseLogo: !!((_b = course === null || course === void 0 ? void 0 : course.course_image) === null || _b === void 0 ? void 0 : _b[0]),
340
+ durationHours: (_c = course === null || course === void 0 ? void 0 : course.duration_hours) !== null && _c !== void 0 ? _c : null,
341
+ progress: (_d = enrollment.progress_percent) !== null && _d !== void 0 ? _d : 0,
342
+ };
343
+ });
344
+ }
345
+ async getWeeklySchedule(cgIds, weekStart, weekEnd) {
346
+ if (cgIds.length === 0)
347
+ return [];
348
+ const sessions = await this.prisma.course_class_session.findMany({
349
+ where: {
350
+ course_class_group_id: { in: cgIds },
351
+ session_date: { gte: weekStart, lte: weekEnd },
352
+ },
353
+ orderBy: [{ session_date: 'asc' }, { start_time: 'asc' }],
354
+ select: {
355
+ id: true,
356
+ session_date: true,
357
+ start_time: true,
358
+ end_time: true,
359
+ location: true,
360
+ course_class_group_id: true,
361
+ course_class_group: { select: { title: true } },
362
+ },
363
+ });
364
+ // Group by day of week
365
+ const byDay = new Map();
366
+ for (const s of sessions) {
367
+ const key = s.session_date.toISOString().slice(0, 10);
368
+ if (!byDay.has(key))
369
+ byDay.set(key, []);
370
+ byDay.get(key).push(s);
371
+ }
372
+ return Array.from(byDay.entries()).map(([date, daySessions]) => ({
373
+ date,
374
+ sessions: daySessions.map((s) => {
375
+ var _a, _b, _c, _d;
376
+ return ({
377
+ sessionId: s.id,
378
+ time: s.start_time,
379
+ endTime: (_a = s.end_time) !== null && _a !== void 0 ? _a : null,
380
+ className: (_c = (_b = s.course_class_group) === null || _b === void 0 ? void 0 : _b.title) !== null && _c !== void 0 ? _c : null,
381
+ location: (_d = s.location) !== null && _d !== void 0 ? _d : null,
382
+ });
383
+ }),
384
+ }));
385
+ }
386
+ async getRanking(personId, cgIds) {
387
+ if (cgIds.length === 0)
388
+ return [];
389
+ const enrollments = await this.prisma.course_enrollment.findMany({
390
+ where: {
391
+ course_class_group_id: { in: cgIds },
392
+ status: { not: 'cancelled' },
393
+ },
394
+ orderBy: { progress_percent: 'desc' },
395
+ take: 10,
396
+ include: {
397
+ person: { select: { id: true, name: true } },
398
+ },
399
+ });
400
+ return enrollments.map((e, index) => {
401
+ var _a, _b, _c;
402
+ return ({
403
+ position: index + 1,
404
+ personId: e.person_id,
405
+ name: (_b = (_a = e.person) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : null,
406
+ progress: (_c = e.progress_percent) !== null && _c !== void 0 ? _c : 0,
407
+ isMe: e.person_id === personId,
408
+ });
409
+ });
410
+ }
411
+ // ── Date helpers ──────────────────────────────────────────────────────────────
412
+ async getGamification(personId, enrollmentIds, weekStart, weekEnd) {
413
+ const attendanceThisWeek = await this.prisma.course_class_attendance.count({
414
+ where: {
415
+ student_id: personId,
416
+ present: true,
417
+ course_class_session: { session_date: { gte: weekStart, lte: weekEnd } },
418
+ },
419
+ });
420
+ const studyHoursThisWeek = Math.round(attendanceThisWeek * 1.5 * 10) / 10;
421
+ // Streak: distinct dates with attendance, ordered desc
422
+ const attendanceDates = await this.prisma.course_class_attendance.findMany({
423
+ where: { student_id: personId, present: true },
424
+ select: { course_class_session: { select: { session_date: true } } },
425
+ orderBy: { course_class_session: { session_date: 'desc' } },
426
+ });
427
+ const uniqueDates = Array.from(new Set(attendanceDates
428
+ .map((a) => { var _a, _b; return (_b = (_a = a.course_class_session) === null || _a === void 0 ? void 0 : _a.session_date) === null || _b === void 0 ? void 0 : _b.toISOString().slice(0, 10); })
429
+ .filter((d) => !!d))).sort((a, b) => b.localeCompare(a));
430
+ let streak = 0;
431
+ if (uniqueDates.length > 0) {
432
+ const today = new Date();
433
+ today.setHours(0, 0, 0, 0);
434
+ let cursor = new Date(today);
435
+ for (const dateStr of uniqueDates) {
436
+ const d = new Date(dateStr + 'T00:00:00');
437
+ const diff = Math.round((cursor.getTime() - d.getTime()) / 86400000);
438
+ if (diff > 1)
439
+ break;
440
+ streak++;
441
+ cursor = d;
442
+ }
443
+ }
444
+ return { streak, studyHoursThisWeek };
445
+ }
446
+ // ── Class-group detail endpoints ─────────────────────────────────────────────
447
+ async resolvePersonIdFromUser(userId) {
448
+ const personUser = await this.prisma.person_user.findFirst({
449
+ where: { user_id: userId },
450
+ select: { person_id: true },
451
+ });
452
+ if (!personUser)
453
+ throw new common_1.NotFoundException('No person profile found for this user');
454
+ return personUser.person_id;
455
+ }
456
+ async assertStudentEnrolled(userId, classGroupId) {
457
+ const personId = await this.resolvePersonIdFromUser(userId);
458
+ const enrollment = await this.prisma.course_enrollment.findFirst({
459
+ where: { person_id: personId, course_class_group_id: classGroupId, status: { not: 'cancelled' } },
460
+ select: { id: true },
461
+ });
462
+ if (!enrollment)
463
+ throw new common_1.NotFoundException('Enrollment not found for this class group');
464
+ return { personId };
465
+ }
466
+ async getClassGroupDetail(userId, classGroupId) {
467
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
468
+ const { personId } = await this.assertStudentEnrolled(userId, classGroupId);
469
+ const now = new Date();
470
+ const [cg, enrollment] = await Promise.all([
471
+ this.prisma.course_class_group.findUnique({
472
+ where: { id: classGroupId },
473
+ include: {
474
+ course: { select: { id: true, title: true } },
475
+ instructor: { select: { person: { select: { name: true } } } },
476
+ _count: { select: { course_enrollment: { where: { status: { not: 'cancelled' } } } } },
477
+ course_class_session: {
478
+ where: { session_date: { gte: now } },
479
+ orderBy: { session_date: 'asc' },
480
+ take: 1,
481
+ select: { id: true, title: true, session_date: true, start_time: true, location: true, meeting_url: true },
482
+ },
483
+ },
484
+ }),
485
+ this.prisma.course_enrollment.findFirst({
486
+ where: { person_id: personId, course_class_group_id: classGroupId },
487
+ select: { id: true, progress_percent: true, final_score: true, status: true },
488
+ }),
489
+ ]);
490
+ if (!cg || !enrollment)
491
+ throw new common_1.NotFoundException(`Class group #${classGroupId} not found`);
492
+ const [totalSessions, pastSessions] = await Promise.all([
493
+ this.prisma.course_class_session.count({ where: { course_class_group_id: classGroupId } }),
494
+ this.prisma.course_class_session.count({ where: { course_class_group_id: classGroupId, session_date: { lt: now } } }),
495
+ ]);
496
+ const myAttendanceCount = await this.prisma.course_class_attendance.count({
497
+ where: { student_id: personId, present: true, course_class_session: { course_class_group_id: classGroupId } },
498
+ });
499
+ const myAttendance = totalSessions > 0 ? this.round((myAttendanceCount / totalSessions) * 100, 0) : 0;
500
+ const allEnrollments = await this.prisma.course_enrollment.findMany({
501
+ where: { course_class_group_id: classGroupId, status: { not: 'cancelled' } },
502
+ orderBy: { progress_percent: 'desc' },
503
+ select: { person_id: true, progress_percent: true },
504
+ });
505
+ const rankingPosition = allEnrollments.findIndex((e) => e.person_id === personId) + 1;
506
+ const totalStudents = allEnrollments.length;
507
+ const HOURS_PER_SESSION = 2;
508
+ const totalHours = totalSessions * HOURS_PER_SESSION;
509
+ const completedHours = pastSessions * HOURS_PER_SESSION;
510
+ const nextSession = (_a = cg.course_class_session[0]) !== null && _a !== void 0 ? _a : null;
511
+ const rankingData = allEnrollments.slice(0, 5).map((e, idx) => {
512
+ var _a;
513
+ return ({
514
+ position: idx + 1,
515
+ personId: e.person_id,
516
+ progress: (_a = e.progress_percent) !== null && _a !== void 0 ? _a : 0,
517
+ isMe: e.person_id === personId,
518
+ });
519
+ });
520
+ if (rankingPosition > 5) {
521
+ rankingData.push({ position: rankingPosition, personId, progress: (_b = enrollment.progress_percent) !== null && _b !== void 0 ? _b : 0, isMe: true });
522
+ }
523
+ return {
524
+ id: cg.id,
525
+ courseName: (_d = (_c = cg.course) === null || _c === void 0 ? void 0 : _c.title) !== null && _d !== void 0 ? _d : null,
526
+ className: cg.title,
527
+ code: (_e = cg.code) !== null && _e !== void 0 ? _e : null,
528
+ instructorName: (_h = (_g = (_f = cg.instructor) === null || _f === void 0 ? void 0 : _f.person) === null || _g === void 0 ? void 0 : _g.name) !== null && _h !== void 0 ? _h : null,
529
+ instructorAvatarUrl: null,
530
+ instructorBio: null,
531
+ startDate: cg.start_date,
532
+ endDate: (_j = cg.end_date) !== null && _j !== void 0 ? _j : null,
533
+ status: cg.status,
534
+ deliveryMode: cg.delivery_mode,
535
+ totalHours,
536
+ completedHours,
537
+ lessonsCompleted: pastSessions,
538
+ totalLessons: totalSessions,
539
+ progress: (_k = enrollment.progress_percent) !== null && _k !== void 0 ? _k : 0,
540
+ myGrade: enrollment.final_score != null ? Number(enrollment.final_score) : null,
541
+ myAttendance,
542
+ rankingPosition: rankingPosition > 0 ? rankingPosition : totalStudents,
543
+ totalStudents,
544
+ rankingData,
545
+ nextSession: nextSession
546
+ ? {
547
+ date: nextSession.session_date,
548
+ time: nextSession.start_time,
549
+ title: nextSession.title,
550
+ location: (_l = nextSession.location) !== null && _l !== void 0 ? _l : null,
551
+ meetingUrl: (_m = nextSession.meeting_url) !== null && _m !== void 0 ? _m : null,
552
+ }
553
+ : null,
554
+ };
555
+ }
556
+ async getClassGroupSessions(userId, classGroupId) {
557
+ await this.assertStudentEnrolled(userId, classGroupId);
558
+ const now = new Date();
559
+ const sessions = await this.prisma.course_class_session.findMany({
560
+ where: { course_class_group_id: classGroupId },
561
+ orderBy: { session_date: 'asc' },
562
+ select: {
563
+ id: true,
564
+ title: true,
565
+ session_date: true,
566
+ start_time: true,
567
+ end_time: true,
568
+ location: true,
569
+ meeting_url: true,
570
+ },
571
+ });
572
+ let hasFoundNext = false;
573
+ return sessions.map((s) => {
574
+ var _a, _b, _c;
575
+ const isPast = s.session_date < now;
576
+ let status;
577
+ if (isPast) {
578
+ status = 'completed';
579
+ }
580
+ else if (!hasFoundNext) {
581
+ status = 'next';
582
+ hasFoundNext = true;
583
+ }
584
+ else {
585
+ status = 'upcoming';
586
+ }
587
+ return {
588
+ id: s.id,
589
+ title: s.title,
590
+ date: s.session_date,
591
+ time: s.start_time,
592
+ endTime: (_a = s.end_time) !== null && _a !== void 0 ? _a : null,
593
+ location: (_b = s.location) !== null && _b !== void 0 ? _b : null,
594
+ meetingUrl: (_c = s.meeting_url) !== null && _c !== void 0 ? _c : null,
595
+ status,
596
+ recordingUrl: null,
597
+ };
598
+ });
599
+ }
600
+ async getClassGroupMaterials(userId, classGroupId) {
601
+ await this.assertStudentEnrolled(userId, classGroupId);
602
+ const materials = await this.prisma.course_class_group_material.findMany({
603
+ where: { course_class_group_id: classGroupId },
604
+ orderBy: [{ sort_order: 'asc' }, { created_at: 'asc' }],
605
+ select: { id: true, title: true, description: true, material_type: true, url: true },
606
+ });
607
+ return materials.map((m) => {
608
+ var _a, _b;
609
+ return ({
610
+ id: m.id,
611
+ title: m.title,
612
+ description: (_a = m.description) !== null && _a !== void 0 ? _a : null,
613
+ materialType: m.material_type,
614
+ url: (_b = m.url) !== null && _b !== void 0 ? _b : null,
615
+ });
616
+ });
617
+ }
618
+ async getMyEvaluations(userId, classGroupId) {
619
+ const { personId } = await this.assertStudentEnrolled(userId, classGroupId);
620
+ const enrollment = await this.prisma.course_enrollment.findFirst({
621
+ where: { person_id: personId, course_class_group_id: classGroupId, status: { not: 'cancelled' } },
622
+ select: { id: true },
623
+ });
624
+ if (!enrollment)
625
+ throw new common_1.NotFoundException('Enrollment not found');
626
+ const attempts = await this.prisma.exam_attempt.findMany({
627
+ where: { course_enrollment_id: enrollment.id },
628
+ orderBy: { started_at: 'desc' },
629
+ include: { exam: { select: { id: true, title: true } } },
630
+ });
631
+ const data = attempts.map((a) => {
632
+ var _a, _b, _c, _d, _e;
633
+ return ({
634
+ id: a.id,
635
+ titulo: (_b = (_a = a.exam) === null || _a === void 0 ? void 0 : _a.title) !== null && _b !== void 0 ? _b : 'Avaliação',
636
+ data: (_d = (_c = a.started_at) === null || _c === void 0 ? void 0 : _c.toISOString().slice(0, 10)) !== null && _d !== void 0 ? _d : null,
637
+ nota: a.score != null ? Number(a.score) : null,
638
+ status: a.score != null ? 'graded' : a.status === 'completed' ? 'pending' : 'in_progress',
639
+ comentario: (_e = a.feedback) !== null && _e !== void 0 ? _e : null,
640
+ });
641
+ });
642
+ const graded = data.filter((e) => e.nota != null);
643
+ const avg = graded.length > 0 ? this.round(graded.reduce((s, e) => { var _a; return s + ((_a = e.nota) !== null && _a !== void 0 ? _a : 0); }, 0) / graded.length, 1) : 0;
644
+ return { avg, total: data.length, graded: graded.length, data };
645
+ }
646
+ round(value, digits = 0) {
647
+ const factor = 10 ** digits;
648
+ return Math.round(value * factor) / factor;
649
+ }
650
+ addDays(date, days) {
651
+ const result = new Date(date);
652
+ result.setDate(result.getDate() + days);
653
+ return result;
654
+ }
655
+ endOfDay(date) {
656
+ const result = new Date(date);
657
+ result.setHours(23, 59, 59, 999);
658
+ return result;
659
+ }
660
+ startOfWeek(date) {
661
+ const result = new Date(date);
662
+ result.setHours(0, 0, 0, 0);
663
+ const day = result.getDay();
664
+ const diff = day === 0 ? -6 : 1 - day;
665
+ result.setDate(result.getDate() + diff);
666
+ return result;
667
+ }
668
+ };
669
+ exports.TrainingStudentService = TrainingStudentService;
670
+ exports.TrainingStudentService = TrainingStudentService = __decorate([
671
+ (0, common_1.Injectable)(),
672
+ __metadata("design:paramtypes", [api_prisma_1.PrismaService])
673
+ ], TrainingStudentService);
674
+ //# sourceMappingURL=training-student.service.js.map