@hed-hog/lms 0.0.331 → 0.0.338
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.
- package/dist/class-group/class-group.controller.d.ts +3 -3
- package/dist/class-group/class-group.service.d.ts +3 -3
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +12 -20
- package/dist/course/course.service.js.map +1 -1
- package/dist/enterprise/enterprise.controller.d.ts +72 -0
- package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
- package/dist/enterprise/enterprise.controller.js +10 -0
- package/dist/enterprise/enterprise.controller.js.map +1 -1
- package/dist/enterprise/enterprise.service.d.ts +78 -0
- package/dist/enterprise/enterprise.service.d.ts.map +1 -1
- package/dist/enterprise/enterprise.service.js +413 -40
- package/dist/enterprise/enterprise.service.js.map +1 -1
- package/dist/enterprise/training/training-admin.controller.d.ts +6 -3
- package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.controller.js +10 -6
- package/dist/enterprise/training/training-admin.controller.js.map +1 -1
- package/dist/enterprise/training/training-admin.service.d.ts +8 -2
- package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.service.js +108 -52
- package/dist/enterprise/training/training-admin.service.js.map +1 -1
- package/dist/enterprise/training/training-viewer.controller.d.ts +3 -0
- package/dist/enterprise/training/training-viewer.controller.d.ts.map +1 -1
- package/dist/evaluation/evaluation.controller.d.ts +4 -4
- package/dist/evaluation/evaluation.service.d.ts +4 -4
- package/dist/instructor/dto/create-instructor-skill.dto.d.ts +0 -4
- package/dist/instructor/dto/create-instructor-skill.dto.d.ts.map +1 -1
- package/dist/instructor/dto/create-instructor-skill.dto.js +0 -21
- package/dist/instructor/dto/create-instructor-skill.dto.js.map +1 -1
- package/dist/instructor/dto/update-instructor-skill.dto.d.ts +0 -4
- package/dist/instructor/dto/update-instructor-skill.dto.d.ts.map +1 -1
- package/dist/instructor/dto/update-instructor-skill.dto.js +0 -22
- package/dist/instructor/dto/update-instructor-skill.dto.js.map +1 -1
- package/dist/instructor/instructor-skill.controller.d.ts +4 -4
- package/dist/instructor/instructor-skill.service.d.ts +4 -7
- package/dist/instructor/instructor-skill.service.d.ts.map +1 -1
- package/dist/instructor/instructor-skill.service.js +2 -89
- package/dist/instructor/instructor-skill.service.js.map +1 -1
- package/dist/instructor/instructor.controller.d.ts +20 -0
- package/dist/instructor/instructor.controller.d.ts.map +1 -1
- package/dist/instructor/instructor.controller.js +19 -0
- package/dist/instructor/instructor.controller.js.map +1 -1
- package/dist/instructor/instructor.service.d.ts +25 -0
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +70 -18
- package/dist/instructor/instructor.service.js.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js.map +1 -1
- package/hedhog/data/route.yaml +23 -1
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +42 -24
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +3 -3
- package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +591 -0
- package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/classes/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +3 -33
- package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +9 -9
- package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +109 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +40 -13
- package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +76 -81
- package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +60 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-scheduled-classes-tab.tsx.ejs +406 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +242 -33
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -0
- package/hedhog/frontend/app/courses/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-activity-timeline.tsx.ejs +87 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +4 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +31 -5
- package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +79 -20
- package/hedhog/frontend/app/enterprise/_components/enterprise-company-identity-card.tsx.ejs +11 -2
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +201 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +55 -24
- package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +430 -296
- package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +277 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-overview-analytics.tsx.ejs +205 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +97 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +82 -57
- package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +4 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +60 -22
- package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +54 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +211 -0
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -7
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/exams/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +51 -104
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +625 -366
- package/hedhog/frontend/app/instructors/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/paths/page.tsx.ejs +9 -4
- package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/training/page.tsx.ejs +9 -4
- package/hedhog/frontend/messages/en.json +101 -10
- package/hedhog/frontend/messages/pt.json +101 -10
- package/hedhog/table/enterprise_student_license_event.yaml +30 -0
- package/hedhog/table/instructor_skill.yaml +0 -11
- package/package.json +7 -7
- package/src/course/course.service.ts +12 -24
- package/src/enterprise/enterprise.controller.ts +5 -0
- package/src/enterprise/enterprise.service.ts +507 -29
- package/src/enterprise/training/training-admin.controller.ts +4 -0
- package/src/enterprise/training/training-admin.service.ts +115 -51
- package/src/instructor/dto/create-instructor-skill.dto.ts +0 -17
- package/src/instructor/dto/update-instructor-skill.dto.ts +0 -18
- package/src/instructor/instructor-skill.service.ts +2 -97
- package/src/instructor/instructor.controller.ts +16 -0
- package/src/instructor/instructor.service.ts +85 -10
- package/src/lms.module.ts +1 -0
|
@@ -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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|