@hed-hog/lms 0.0.331 → 0.0.347

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 (135) hide show
  1. package/dist/class-group/class-group.controller.d.ts +8 -8
  2. package/dist/class-group/class-group.service.d.ts +8 -8
  3. package/dist/course/course.controller.d.ts +6 -1
  4. package/dist/course/course.controller.d.ts.map +1 -1
  5. package/dist/course/course.controller.js +19 -2
  6. package/dist/course/course.controller.js.map +1 -1
  7. package/dist/course/course.service.d.ts +6 -0
  8. package/dist/course/course.service.d.ts.map +1 -1
  9. package/dist/course/course.service.js +63 -28
  10. package/dist/course/course.service.js.map +1 -1
  11. package/dist/course/dto/create-course.dto.d.ts +1 -0
  12. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  13. package/dist/course/dto/create-course.dto.js +5 -0
  14. package/dist/course/dto/create-course.dto.js.map +1 -1
  15. package/dist/enterprise/enterprise.controller.d.ts +84 -12
  16. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  17. package/dist/enterprise/enterprise.controller.js +10 -0
  18. package/dist/enterprise/enterprise.controller.js.map +1 -1
  19. package/dist/enterprise/enterprise.service.d.ts +90 -12
  20. package/dist/enterprise/enterprise.service.d.ts.map +1 -1
  21. package/dist/enterprise/enterprise.service.js +413 -40
  22. package/dist/enterprise/enterprise.service.js.map +1 -1
  23. package/dist/enterprise/training/training-admin.controller.d.ts +9 -6
  24. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  25. package/dist/enterprise/training/training-admin.controller.js +10 -6
  26. package/dist/enterprise/training/training-admin.controller.js.map +1 -1
  27. package/dist/enterprise/training/training-admin.service.d.ts +11 -5
  28. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  29. package/dist/enterprise/training/training-admin.service.js +108 -52
  30. package/dist/enterprise/training/training-admin.service.js.map +1 -1
  31. package/dist/enterprise/training/training-viewer.controller.d.ts +3 -0
  32. package/dist/enterprise/training/training-viewer.controller.d.ts.map +1 -1
  33. package/dist/evaluation/evaluation.controller.d.ts +2 -2
  34. package/dist/evaluation/evaluation.service.d.ts +2 -2
  35. package/dist/instructor/dto/create-instructor-skill.dto.d.ts +0 -4
  36. package/dist/instructor/dto/create-instructor-skill.dto.d.ts.map +1 -1
  37. package/dist/instructor/dto/create-instructor-skill.dto.js +0 -21
  38. package/dist/instructor/dto/create-instructor-skill.dto.js.map +1 -1
  39. package/dist/instructor/dto/update-instructor-skill.dto.d.ts +0 -4
  40. package/dist/instructor/dto/update-instructor-skill.dto.d.ts.map +1 -1
  41. package/dist/instructor/dto/update-instructor-skill.dto.js +0 -22
  42. package/dist/instructor/dto/update-instructor-skill.dto.js.map +1 -1
  43. package/dist/instructor/instructor-skill.controller.d.ts +4 -4
  44. package/dist/instructor/instructor-skill.service.d.ts +4 -7
  45. package/dist/instructor/instructor-skill.service.d.ts.map +1 -1
  46. package/dist/instructor/instructor-skill.service.js +2 -89
  47. package/dist/instructor/instructor-skill.service.js.map +1 -1
  48. package/dist/instructor/instructor.controller.d.ts +21 -0
  49. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  50. package/dist/instructor/instructor.controller.js +19 -0
  51. package/dist/instructor/instructor.controller.js.map +1 -1
  52. package/dist/instructor/instructor.service.d.ts +27 -0
  53. package/dist/instructor/instructor.service.d.ts.map +1 -1
  54. package/dist/instructor/instructor.service.js +79 -25
  55. package/dist/instructor/instructor.service.js.map +1 -1
  56. package/dist/lms.module.d.ts.map +1 -1
  57. package/dist/lms.module.js.map +1 -1
  58. package/dist/training/dto/create-training.dto.d.ts +1 -0
  59. package/dist/training/dto/create-training.dto.d.ts.map +1 -1
  60. package/dist/training/dto/create-training.dto.js +5 -0
  61. package/dist/training/dto/create-training.dto.js.map +1 -1
  62. package/dist/training/training.controller.d.ts +4 -0
  63. package/dist/training/training.controller.d.ts.map +1 -1
  64. package/dist/training/training.service.d.ts +8 -0
  65. package/dist/training/training.service.d.ts.map +1 -1
  66. package/dist/training/training.service.js +71 -6
  67. package/dist/training/training.service.js.map +1 -1
  68. package/hedhog/data/route.yaml +23 -1
  69. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +80 -33
  70. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +3 -3
  71. package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +591 -0
  72. package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +6 -1
  73. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +39 -7
  74. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +1 -3
  75. package/hedhog/frontend/app/classes/page.tsx.ejs +34 -7
  76. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +3 -33
  77. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +9 -9
  78. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +109 -0
  79. package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +40 -13
  80. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +76 -81
  81. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +60 -0
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/course-scheduled-classes-tab.tsx.ejs +406 -0
  83. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
  84. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
  85. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
  86. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
  87. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +243 -34
  88. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -0
  89. package/hedhog/frontend/app/courses/page.tsx.ejs +6 -1
  90. package/hedhog/frontend/app/enterprise/_components/enterprise-activity-timeline.tsx.ejs +87 -0
  91. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +4 -0
  92. package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +31 -5
  93. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +79 -20
  94. package/hedhog/frontend/app/enterprise/_components/enterprise-company-identity-card.tsx.ejs +11 -2
  95. package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +201 -0
  96. package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +55 -24
  97. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +430 -296
  98. package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +277 -0
  99. package/hedhog/frontend/app/enterprise/_components/enterprise-overview-analytics.tsx.ejs +205 -0
  100. package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +97 -0
  101. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +82 -57
  102. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +4 -0
  103. package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +60 -22
  104. package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +54 -0
  105. package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +211 -0
  106. package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -7
  107. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +31 -19
  108. package/hedhog/frontend/app/exams/page.tsx.ejs +6 -1
  109. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +51 -104
  110. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +625 -366
  111. package/hedhog/frontend/app/instructors/page.tsx.ejs +6 -1
  112. package/hedhog/frontend/app/paths/page.tsx.ejs +76 -8
  113. package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +6 -1
  114. package/hedhog/frontend/app/training/page.tsx.ejs +78 -9
  115. package/hedhog/frontend/messages/en.json +101 -10
  116. package/hedhog/frontend/messages/pt.json +115 -11
  117. package/hedhog/table/enterprise_student_license_event.yaml +30 -0
  118. package/hedhog/table/instructor_skill.yaml +0 -11
  119. package/hedhog/table/learning_path.yaml +4 -0
  120. package/package.json +6 -6
  121. package/src/course/course.controller.ts +18 -0
  122. package/src/course/course.service.ts +85 -26
  123. package/src/course/dto/create-course.dto.ts +4 -0
  124. package/src/enterprise/enterprise.controller.ts +5 -0
  125. package/src/enterprise/enterprise.service.ts +507 -29
  126. package/src/enterprise/training/training-admin.controller.ts +4 -0
  127. package/src/enterprise/training/training-admin.service.ts +115 -51
  128. package/src/instructor/dto/create-instructor-skill.dto.ts +0 -17
  129. package/src/instructor/dto/update-instructor-skill.dto.ts +0 -18
  130. package/src/instructor/instructor-skill.service.ts +2 -97
  131. package/src/instructor/instructor.controller.ts +16 -0
  132. package/src/instructor/instructor.service.ts +87 -10
  133. package/src/lms.module.ts +1 -0
  134. package/src/training/dto/create-training.dto.ts +4 -0
  135. package/src/training/training.service.ts +104 -5
@@ -23,6 +23,8 @@ type PaginationParams = {
23
23
  crmPersonId?: number;
24
24
  };
25
25
 
26
+ type LicenseEventType = 'assigned' | 'revoked' | 'status_changed';
27
+
26
28
  /** Maps enterprise_user.role to the global role slug that should be granted. */
27
29
  const ENTERPRISE_ROLE_TO_GLOBAL_SLUG: Partial<Record<string, string>> = {
28
30
  enterprise_admin: 'lms-enterprise-admin',
@@ -96,6 +98,7 @@ export class EnterpriseService {
96
98
  id: true,
97
99
  name: true,
98
100
  type: true,
101
+ avatar_id: true,
99
102
  },
100
103
  },
101
104
  _count: {
@@ -155,6 +158,175 @@ export class EnterpriseService {
155
158
  return this.prisma.enterprise.update({ where: { id }, data: dto as any });
156
159
  }
157
160
 
161
+ async getOverview(id: number) {
162
+ const enterprise = await this.getById(id);
163
+ if (!enterprise) return null;
164
+
165
+ const eventModel = (this.prisma as any).enterprise_student_license_event;
166
+ const now = new Date();
167
+ const timelineStart = new Date(
168
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 11, 1),
169
+ );
170
+
171
+ const [
172
+ studentRows,
173
+ scheduledClasses,
174
+ licenseEvents,
175
+ recentCourses,
176
+ recentClasses,
177
+ recentStudents,
178
+ recentAdmins,
179
+ ] = await Promise.all([
180
+ this.prisma.enterprise_student.findMany({
181
+ where: { enterprise_id: id },
182
+ select: {
183
+ person_id: true,
184
+ status: true,
185
+ created_at: true,
186
+ person: { select: { id: true, name: true } },
187
+ },
188
+ }),
189
+ this.prisma.enterprise_class_group.findMany({
190
+ where: {
191
+ enterprise_id: id,
192
+ course_class_group: { status: { in: ['open', 'ongoing'] as any[] } },
193
+ },
194
+ select: {
195
+ created_at: true,
196
+ course_class_group: {
197
+ select: {
198
+ id: true,
199
+ code: true,
200
+ title: true,
201
+ status: true,
202
+ capacity: true,
203
+ course: { select: { title: true } },
204
+ _count: {
205
+ select: {
206
+ course_enrollment: {
207
+ where: { status: { not: 'cancelled' } },
208
+ },
209
+ },
210
+ },
211
+ },
212
+ },
213
+ },
214
+ }),
215
+ eventModel
216
+ ? eventModel
217
+ .findMany({
218
+ where: { enterprise_id: id },
219
+ orderBy: { created_at: 'asc' },
220
+ select: {
221
+ person_id: true,
222
+ event_type: true,
223
+ previous_status: true,
224
+ next_status: true,
225
+ created_at: true,
226
+ person: { select: { id: true, name: true } },
227
+ },
228
+ })
229
+ .catch(() => [])
230
+ : Promise.resolve([]),
231
+ this.prisma.enterprise_course.findMany({
232
+ where: { enterprise_id: id },
233
+ take: 5,
234
+ orderBy: { created_at: 'desc' },
235
+ select: {
236
+ created_at: true,
237
+ course: { select: { id: true, title: true } },
238
+ },
239
+ }),
240
+ this.prisma.enterprise_class_group.findMany({
241
+ where: { enterprise_id: id },
242
+ take: 5,
243
+ orderBy: { created_at: 'desc' },
244
+ select: {
245
+ created_at: true,
246
+ course_class_group: {
247
+ select: {
248
+ id: true,
249
+ code: true,
250
+ title: true,
251
+ course: { select: { title: true } },
252
+ },
253
+ },
254
+ },
255
+ }),
256
+ this.prisma.enterprise_student.findMany({
257
+ where: { enterprise_id: id },
258
+ take: 5,
259
+ orderBy: { created_at: 'desc' },
260
+ select: {
261
+ created_at: true,
262
+ person: { select: { id: true, name: true } },
263
+ },
264
+ }),
265
+ this.prisma.enterprise_user.findMany({
266
+ where: { enterprise_id: id },
267
+ take: 5,
268
+ orderBy: { created_at: 'desc' },
269
+ select: {
270
+ created_at: true,
271
+ role: true,
272
+ user: { select: { id: true, name: true } },
273
+ },
274
+ }),
275
+ ]);
276
+
277
+ const used = studentRows.length;
278
+ const limit = enterprise.licenseLimit;
279
+ const available =
280
+ limit === null ? null : Math.max((limit as number) - used, 0);
281
+ const percent =
282
+ limit && limit > 0 ? Math.min(100, Math.round((used / limit) * 100)) : 0;
283
+
284
+ const capacity = scheduledClasses.reduce(
285
+ (sum, row) => sum + Math.max(row.course_class_group?.capacity ?? 0, 0),
286
+ 0,
287
+ );
288
+ const seatsUsed = scheduledClasses.reduce(
289
+ (sum, row) =>
290
+ sum + (row.course_class_group?._count?.course_enrollment ?? 0),
291
+ 0,
292
+ );
293
+
294
+ return {
295
+ account: enterprise,
296
+ kpis: {
297
+ students: enterprise.studentsCount,
298
+ classes: enterprise.classesCount,
299
+ courses: enterprise.coursesCount,
300
+ administrators: enterprise.adminsCount + enterprise.managersCount,
301
+ portalEnabled: enterprise.portalEnabled,
302
+ },
303
+ licenseUsage: {
304
+ used,
305
+ limit,
306
+ available,
307
+ percent,
308
+ },
309
+ scheduledSeats: {
310
+ used: seatsUsed,
311
+ open: Math.max(capacity - seatsUsed, 0),
312
+ capacity,
313
+ },
314
+ licenseTimeline: this.buildLicenseTimeline(
315
+ studentRows,
316
+ licenseEvents,
317
+ timelineStart,
318
+ now,
319
+ ),
320
+ activities: this.buildEnterpriseActivities(
321
+ licenseEvents,
322
+ recentCourses,
323
+ recentClasses,
324
+ recentStudents,
325
+ recentAdmins,
326
+ ),
327
+ };
328
+ }
329
+
158
330
  async delete(id: number) {
159
331
  await this.assertEnterpriseExists(id);
160
332
  return this.prisma.enterprise.delete({ where: { id } });
@@ -171,6 +343,217 @@ export class EnterpriseService {
171
343
  return exists;
172
344
  }
173
345
 
346
+ private async recordLicenseEvent(params: {
347
+ enterpriseId: number;
348
+ personId: number;
349
+ eventType: LicenseEventType;
350
+ previousStatus?: string | null;
351
+ nextStatus?: string | null;
352
+ }) {
353
+ const eventModel = (this.prisma as any).enterprise_student_license_event;
354
+ if (!eventModel) return;
355
+ try {
356
+ await eventModel.create({
357
+ data: {
358
+ enterprise_id: params.enterpriseId,
359
+ person_id: params.personId,
360
+ event_type: params.eventType,
361
+ previous_status: params.previousStatus ?? null,
362
+ next_status: params.nextStatus ?? null,
363
+ },
364
+ });
365
+ } catch {
366
+ // The YAML/DB apply step may not have run yet in older environments.
367
+ }
368
+ }
369
+
370
+ private monthKey(date: Date) {
371
+ const year = date.getUTCFullYear();
372
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
373
+ return `${year}-${month}`;
374
+ }
375
+
376
+ private monthLabel(date: Date) {
377
+ return date.toLocaleDateString('pt-BR', {
378
+ month: 'short',
379
+ year: '2-digit',
380
+ timeZone: 'UTC',
381
+ });
382
+ }
383
+
384
+ private buildLicenseTimeline(
385
+ studentRows: Array<{
386
+ person_id: number;
387
+ created_at: Date;
388
+ }>,
389
+ eventRows: Array<{
390
+ person_id: number;
391
+ event_type: string;
392
+ previous_status?: string | null;
393
+ next_status?: string | null;
394
+ created_at: Date;
395
+ }>,
396
+ start: Date,
397
+ end: Date,
398
+ ) {
399
+ const eventsByPerson = new Set(
400
+ eventRows
401
+ .filter((event) => event.event_type === 'assigned')
402
+ .map((event) => event.person_id),
403
+ );
404
+ const syntheticEvents = studentRows
405
+ .filter((student) => !eventsByPerson.has(student.person_id))
406
+ .map((student) => ({
407
+ person_id: student.person_id,
408
+ event_type: 'assigned',
409
+ previous_status: null,
410
+ next_status: 'active',
411
+ created_at: student.created_at,
412
+ }));
413
+
414
+ const allEvents = [...eventRows, ...syntheticEvents].sort(
415
+ (a, b) => a.created_at.getTime() - b.created_at.getTime(),
416
+ );
417
+
418
+ let running = 0;
419
+ for (const event of allEvents) {
420
+ if (event.created_at >= start) break;
421
+ running += this.getLicenseEventDelta(event);
422
+ running = Math.max(running, 0);
423
+ }
424
+
425
+ const points: Array<{
426
+ period: string;
427
+ label: string;
428
+ used: number;
429
+ assigned: number;
430
+ revoked: number;
431
+ }> = [];
432
+
433
+ const cursor = new Date(
434
+ Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), 1),
435
+ );
436
+ while (cursor <= end) {
437
+ const key = this.monthKey(cursor);
438
+ const monthEvents = allEvents.filter(
439
+ (event) => this.monthKey(event.created_at) === key,
440
+ );
441
+ const assigned = monthEvents.filter(
442
+ (event) => this.getLicenseEventDelta(event) > 0,
443
+ ).length;
444
+ const revoked = monthEvents.filter(
445
+ (event) => this.getLicenseEventDelta(event) < 0,
446
+ ).length;
447
+ for (const event of monthEvents) {
448
+ running += this.getLicenseEventDelta(event);
449
+ running = Math.max(running, 0);
450
+ }
451
+ points.push({
452
+ period: key,
453
+ label: this.monthLabel(cursor),
454
+ used: running,
455
+ assigned,
456
+ revoked,
457
+ });
458
+ cursor.setUTCMonth(cursor.getUTCMonth() + 1);
459
+ }
460
+
461
+ return points;
462
+ }
463
+
464
+ private getLicenseEventDelta(event: {
465
+ event_type: string;
466
+ previous_status?: string | null;
467
+ next_status?: string | null;
468
+ }) {
469
+ if (event.event_type === 'assigned') return 1;
470
+ if (event.event_type === 'revoked') return -1;
471
+ if (event.event_type === 'status_changed') {
472
+ const wasActive = event.previous_status !== 'inactive';
473
+ const isActive = event.next_status !== 'inactive';
474
+ if (!wasActive && isActive) return 1;
475
+ if (wasActive && !isActive) return -1;
476
+ }
477
+ return 0;
478
+ }
479
+
480
+ private buildEnterpriseActivities(
481
+ licenseEvents: Array<{
482
+ event_type: string;
483
+ created_at: Date;
484
+ person?: { id: number; name: string | null } | null;
485
+ }>,
486
+ courses: Array<{
487
+ created_at: Date;
488
+ course?: { id: number; title: string | null } | null;
489
+ }>,
490
+ classes: Array<{
491
+ created_at: Date;
492
+ course_class_group?: {
493
+ id: number;
494
+ code: string | null;
495
+ title: string | null;
496
+ course?: { title: string | null } | null;
497
+ } | null;
498
+ }>,
499
+ students: Array<{
500
+ created_at: Date;
501
+ person?: { id: number; name: string | null } | null;
502
+ }>,
503
+ admins: Array<{
504
+ created_at: Date;
505
+ role: string;
506
+ user?: { id: number; name: string | null } | null;
507
+ }>,
508
+ ) {
509
+ return [
510
+ ...licenseEvents.slice(-10).map((event) => ({
511
+ id: `license-${event.person?.id ?? 'unknown'}-${event.created_at.getTime()}`,
512
+ type: event.event_type,
513
+ title:
514
+ event.event_type === 'revoked'
515
+ ? 'Licenca removida'
516
+ : 'Licenca atribuida',
517
+ description: event.person?.name ?? 'Aluno',
518
+ createdAt: event.created_at,
519
+ })),
520
+ ...courses.map((row) => ({
521
+ id: `course-${row.course?.id ?? 'unknown'}-${row.created_at.getTime()}`,
522
+ type: 'course',
523
+ title: 'Curso vinculado',
524
+ description: row.course?.title ?? 'Curso',
525
+ createdAt: row.created_at,
526
+ })),
527
+ ...classes.map((row) => ({
528
+ id: `class-${row.course_class_group?.id ?? 'unknown'}-${row.created_at.getTime()}`,
529
+ type: 'class',
530
+ title: 'Turma vinculada',
531
+ description:
532
+ row.course_class_group?.code ??
533
+ row.course_class_group?.title ??
534
+ row.course_class_group?.course?.title ??
535
+ 'Turma',
536
+ createdAt: row.created_at,
537
+ })),
538
+ ...students.map((row) => ({
539
+ id: `student-${row.person?.id ?? 'unknown'}-${row.created_at.getTime()}`,
540
+ type: 'student',
541
+ title: 'Aluno adicionado',
542
+ description: row.person?.name ?? 'Aluno',
543
+ createdAt: row.created_at,
544
+ })),
545
+ ...admins.map((row) => ({
546
+ id: `admin-${row.user?.id ?? 'unknown'}-${row.created_at.getTime()}`,
547
+ type: 'admin',
548
+ title: 'Administrador adicionado',
549
+ description: row.user?.name ? `${row.user.name} (${row.role})` : row.role,
550
+ createdAt: row.created_at,
551
+ })),
552
+ ]
553
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
554
+ .slice(0, 12);
555
+ }
556
+
174
557
  private async resolveRoleId(slug: string): Promise<number | null> {
175
558
  const role = await this.prisma.role.findUnique({
176
559
  where: { slug },
@@ -240,6 +623,7 @@ export class EnterpriseService {
240
623
  id: true,
241
624
  name: true,
242
625
  last_login_at: true,
626
+ photo_id: true,
243
627
  user_identifier: {
244
628
  where: { type: 'email' },
245
629
  select: { value: true },
@@ -263,6 +647,7 @@ export class EnterpriseService {
263
647
  personId: r.person_id,
264
648
  name: r.user?.name ?? null,
265
649
  email: r.user?.user_identifier?.[0]?.value ?? null,
650
+ photoId: r.user?.photo_id ?? null,
266
651
  role: r.role,
267
652
  status: r.status,
268
653
  lastAccessAt: r.user?.last_login_at ?? null,
@@ -380,6 +765,16 @@ export class EnterpriseService {
380
765
  status: true,
381
766
  level: true,
382
767
  offering_type: true,
768
+ course_image: {
769
+ where: { image_type: { slug: 'course-logo' } },
770
+ take: 1,
771
+ select: {
772
+ file_id: true,
773
+ file: {
774
+ select: { id: true },
775
+ },
776
+ },
777
+ },
383
778
  },
384
779
  },
385
780
  },
@@ -392,16 +787,21 @@ export class EnterpriseService {
392
787
  page,
393
788
  pageSize,
394
789
  lastPage: Math.max(1, Math.ceil(total / pageSize)),
395
- data: rows.map((r) => ({
396
- id: r.id,
397
- courseId: r.course_id,
398
- contractedAt: r.contracted_at,
399
- title: r.course?.title ?? null,
400
- slug: r.course?.slug ?? null,
401
- status: r.course?.status ?? null,
402
- level: r.course?.level ?? null,
403
- modality: r.course?.offering_type ?? null,
404
- })),
790
+ data: rows.map((r) => {
791
+ const courseLogo = r.course?.course_image?.[0] ?? null;
792
+
793
+ return {
794
+ id: r.id,
795
+ courseId: r.course_id,
796
+ contractedAt: r.contracted_at,
797
+ title: r.course?.title ?? null,
798
+ slug: r.course?.slug ?? null,
799
+ status: r.course?.status ?? null,
800
+ level: r.course?.level ?? null,
801
+ modality: r.course?.offering_type ?? null,
802
+ logoFileId: courseLogo?.file?.id ?? courseLogo?.file_id ?? null,
803
+ };
804
+ }),
405
805
  };
406
806
  }
407
807
 
@@ -481,7 +881,43 @@ export class EnterpriseService {
481
881
  start_date: true,
482
882
  end_date: true,
483
883
  capacity: true,
484
- course: { select: { id: true, title: true, slug: true } },
884
+ _count: {
885
+ select: {
886
+ course_enrollment: {
887
+ where: {
888
+ status: { not: 'cancelled' },
889
+ },
890
+ },
891
+ },
892
+ },
893
+ instructor: {
894
+ select: {
895
+ id: true,
896
+ person: {
897
+ select: {
898
+ name: true,
899
+ avatar_id: true,
900
+ },
901
+ },
902
+ },
903
+ },
904
+ course: {
905
+ select: {
906
+ id: true,
907
+ title: true,
908
+ slug: true,
909
+ course_image: {
910
+ where: { image_type: { slug: 'course-logo' } },
911
+ take: 1,
912
+ select: {
913
+ file_id: true,
914
+ file: {
915
+ select: { id: true },
916
+ },
917
+ },
918
+ },
919
+ },
920
+ },
485
921
  },
486
922
  },
487
923
  },
@@ -494,19 +930,28 @@ export class EnterpriseService {
494
930
  page,
495
931
  pageSize,
496
932
  lastPage: Math.max(1, Math.ceil(total / pageSize)),
497
- data: rows.map((r) => ({
498
- id: r.id,
499
- courseClassGroupId: r.course_class_group_id,
500
- code: r.course_class_group?.code ?? null,
501
- title: r.course_class_group?.title ?? null,
502
- status: r.course_class_group?.status ?? null,
503
- deliveryMode: r.course_class_group?.delivery_mode ?? null,
504
- startDate: r.course_class_group?.start_date ?? null,
505
- endDate: r.course_class_group?.end_date ?? null,
506
- capacity: r.course_class_group?.capacity ?? null,
507
- courseTitle: r.course_class_group?.course?.title ?? null,
508
- courseSlug: r.course_class_group?.course?.slug ?? null,
509
- })),
933
+ data: rows.map((r) => {
934
+ const courseLogo = r.course_class_group?.course?.course_image?.[0] ?? null;
935
+
936
+ return {
937
+ id: r.id,
938
+ courseClassGroupId: r.course_class_group_id,
939
+ code: r.course_class_group?.code ?? null,
940
+ title: r.course_class_group?.title ?? null,
941
+ status: r.course_class_group?.status ?? null,
942
+ deliveryMode: r.course_class_group?.delivery_mode ?? null,
943
+ startDate: r.course_class_group?.start_date ?? null,
944
+ endDate: r.course_class_group?.end_date ?? null,
945
+ capacity: r.course_class_group?.capacity ?? null,
946
+ enrolledCount: r.course_class_group?._count?.course_enrollment ?? 0,
947
+ instructorId: r.course_class_group?.instructor?.id ?? null,
948
+ instructorName: r.course_class_group?.instructor?.person?.name ?? null,
949
+ instructorAvatarId: r.course_class_group?.instructor?.person?.avatar_id ?? null,
950
+ courseTitle: r.course_class_group?.course?.title ?? null,
951
+ courseSlug: r.course_class_group?.course?.slug ?? null,
952
+ logoFileId: courseLogo?.file?.id ?? courseLogo?.file_id ?? null,
953
+ };
954
+ }),
510
955
  };
511
956
  }
512
957
 
@@ -554,6 +999,12 @@ export class EnterpriseService {
554
999
  await this.prisma.enterprise_student.create({
555
1000
  data: { enterprise_id: enterpriseId, person_id, status: 'active' },
556
1001
  });
1002
+ await this.recordLicenseEvent({
1003
+ enterpriseId,
1004
+ personId: person_id,
1005
+ eventType: 'assigned',
1006
+ nextStatus: 'active',
1007
+ });
557
1008
  }
558
1009
  }
559
1010
 
@@ -601,6 +1052,7 @@ export class EnterpriseService {
601
1052
  select: {
602
1053
  id: true,
603
1054
  name: true,
1055
+ avatar_id: true,
604
1056
  contact: {
605
1057
  where: { contact_type: { code: 'EMAIL' }, is_primary: true },
606
1058
  select: { value: true },
@@ -623,6 +1075,7 @@ export class EnterpriseService {
623
1075
  personId: r.person_id,
624
1076
  name: r.person?.name ?? null,
625
1077
  email: r.person?.contact?.[0]?.value ?? null,
1078
+ avatarId: r.person?.avatar_id ?? null,
626
1079
  status: r.status,
627
1080
  createdAt: r.created_at,
628
1081
  })),
@@ -647,9 +1100,16 @@ export class EnterpriseService {
647
1100
  `Person #${dto.person_id} is already a student of enterprise #${enterpriseId}`,
648
1101
  );
649
1102
 
650
- return this.prisma.enterprise_student.create({
1103
+ const created = await this.prisma.enterprise_student.create({
651
1104
  data: { enterprise_id: enterpriseId, ...(dto as any) },
652
1105
  });
1106
+ await this.recordLicenseEvent({
1107
+ enterpriseId,
1108
+ personId: dto.person_id,
1109
+ eventType: 'assigned',
1110
+ nextStatus: (dto as any).status ?? 'pending',
1111
+ });
1112
+ return created;
653
1113
  }
654
1114
 
655
1115
  async updateStudent(
@@ -660,31 +1120,48 @@ export class EnterpriseService {
660
1120
  await this.assertEnterpriseExists(enterpriseId);
661
1121
  const existing = await this.prisma.enterprise_student.findFirst({
662
1122
  where: { enterprise_id: enterpriseId, person_id: personId },
663
- select: { id: true },
1123
+ select: { id: true, status: true },
664
1124
  });
665
1125
  if (!existing)
666
1126
  throw new NotFoundException(
667
1127
  `Person #${personId} is not a student of enterprise #${enterpriseId}`,
668
1128
  );
669
- return this.prisma.enterprise_student.update({
1129
+ const updated = await this.prisma.enterprise_student.update({
670
1130
  where: { id: existing.id },
671
1131
  data: dto as any,
672
1132
  });
1133
+ if (dto.status && dto.status !== existing.status) {
1134
+ await this.recordLicenseEvent({
1135
+ enterpriseId,
1136
+ personId,
1137
+ eventType: 'status_changed',
1138
+ previousStatus: existing.status,
1139
+ nextStatus: dto.status,
1140
+ });
1141
+ }
1142
+ return updated;
673
1143
  }
674
1144
 
675
1145
  async removeStudent(enterpriseId: number, personId: number) {
676
1146
  await this.assertEnterpriseExists(enterpriseId);
677
1147
  const existing = await this.prisma.enterprise_student.findFirst({
678
1148
  where: { enterprise_id: enterpriseId, person_id: personId },
679
- select: { id: true },
1149
+ select: { id: true, status: true },
680
1150
  });
681
1151
  if (!existing)
682
1152
  throw new NotFoundException(
683
1153
  `Person #${personId} is not a student of enterprise #${enterpriseId}`,
684
1154
  );
685
- return this.prisma.enterprise_student.delete({
1155
+ const deleted = await this.prisma.enterprise_student.delete({
686
1156
  where: { id: existing.id },
687
1157
  });
1158
+ await this.recordLicenseEvent({
1159
+ enterpriseId,
1160
+ personId,
1161
+ eventType: 'revoked',
1162
+ previousStatus: existing.status,
1163
+ });
1164
+ return deleted;
688
1165
  }
689
1166
 
690
1167
  // ─── Stats / options ──────────────────────────────────────────────────────────
@@ -921,6 +1398,7 @@ export class EnterpriseService {
921
1398
  ? {
922
1399
  id: person.id as number,
923
1400
  name: person.name as string,
1401
+ avatarId: (person.avatar_id as number | null) ?? null,
924
1402
  tradeName: (pc?.trade_name as string | null) ?? null,
925
1403
  industry: (pc?.industry as string | null) ?? null,
926
1404
  website: (pc?.website as string | null) ?? null,
@@ -49,6 +49,8 @@ export class TrainingAdminController {
49
49
  getClassGroups(
50
50
  @User('id') userId: number,
51
51
  @Query('enterpriseId', new ParseIntPipe({ optional: true })) enterpriseId?: number,
52
+ @Query('page', new ParseIntPipe({ optional: true })) page?: number,
53
+ @Query('pageSize', new ParseIntPipe({ optional: true })) pageSize?: number,
52
54
  @Query('search') search?: string,
53
55
  @Query('status') status?: string,
54
56
  @Query('deliveryMode') deliveryMode?: string,
@@ -56,6 +58,8 @@ export class TrainingAdminController {
56
58
  ) {
57
59
  return this.trainingAdminService.getClassGroups(userId, {
58
60
  enterpriseId,
61
+ page,
62
+ pageSize,
59
63
  search,
60
64
  status,
61
65
  deliveryMode,