@hed-hog/lms 0.0.305 → 0.0.306
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/course/course.service.js +4 -4
- package/hedhog/data/dashboard_component.yaml +152 -152
- package/hedhog/data/dashboard_item.yaml +166 -166
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +317 -317
- package/hedhog/frontend/app/enterprise/_components/enterprise-class-create-sheet.tsx.ejs +1 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +12 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-related-tab.tsx.ejs +44 -7
- package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +96 -96
- package/hedhog/frontend/app/page.tsx.ejs +5 -5
- package/hedhog/table/course.yaml +15 -15
- package/package.json +7 -7
- package/src/class-group/class-group.service.ts +413 -413
- package/src/class-group/dto/create-class-group.dto.ts +77 -77
- package/src/course/course.service.ts +165 -165
- package/src/course/dto/create-course.dto.ts +15 -15
- package/dist/enterprise/dto/add-enterprise-lead.dto.d.ts +0 -4
- package/dist/enterprise/dto/add-enterprise-lead.dto.d.ts.map +0 -1
- package/dist/enterprise/dto/add-enterprise-lead.dto.js +0 -22
- package/dist/enterprise/dto/add-enterprise-lead.dto.js.map +0 -1
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
2
|
-
import {
|
|
3
|
-
BadRequestException,
|
|
4
|
-
ConflictException,
|
|
5
|
-
Injectable,
|
|
6
|
-
NotFoundException,
|
|
7
|
-
} from '@nestjs/common';
|
|
8
|
-
import { Prisma } from '@prisma/client';
|
|
9
|
-
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
|
10
|
-
import { randomUUID } from 'node:crypto';
|
|
11
|
-
import {
|
|
12
|
-
CreateClassGroupDto,
|
|
13
|
-
type ClassGroupSessionRecurrenceDto,
|
|
14
|
-
type ClassGroupSessionTemplateDto,
|
|
15
|
-
} from './dto/create-class-group.dto';
|
|
16
|
-
import { CreateSessionDto, type SessionRecurrenceDto } from './dto/create-session.dto';
|
|
17
|
-
import { UpdateClassGroupDto } from './dto/update-class-group.dto';
|
|
18
|
-
import { UpdateSessionDto } from './dto/update-session.dto';
|
|
2
|
+
import {
|
|
3
|
+
BadRequestException,
|
|
4
|
+
ConflictException,
|
|
5
|
+
Injectable,
|
|
6
|
+
NotFoundException,
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import { Prisma } from '@prisma/client';
|
|
9
|
+
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import {
|
|
12
|
+
CreateClassGroupDto,
|
|
13
|
+
type ClassGroupSessionRecurrenceDto,
|
|
14
|
+
type ClassGroupSessionTemplateDto,
|
|
15
|
+
} from './dto/create-class-group.dto';
|
|
16
|
+
import { CreateSessionDto, type SessionRecurrenceDto } from './dto/create-session.dto';
|
|
17
|
+
import { UpdateClassGroupDto } from './dto/update-class-group.dto';
|
|
18
|
+
import { UpdateSessionDto } from './dto/update-session.dto';
|
|
19
19
|
|
|
20
20
|
type SessionScope = 'single' | 'series';
|
|
21
21
|
type SessionRecurrenceDay = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
|
|
22
22
|
|
|
23
|
-
type NormalizedSessionRecurrence = {
|
|
24
|
-
frequency: 'daily' | 'weekly' | 'monthly' | 'yearly';
|
|
25
|
-
interval: number;
|
|
26
|
-
until: string;
|
|
27
|
-
daysOfWeek?: SessionRecurrenceDay[];
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
type ClassSessionTemplateSummary = {
|
|
31
|
-
title: string;
|
|
32
|
-
isRecurring: boolean;
|
|
33
|
-
recurrence?: NormalizedSessionRecurrence | null;
|
|
34
|
-
};
|
|
23
|
+
type NormalizedSessionRecurrence = {
|
|
24
|
+
frequency: 'daily' | 'weekly' | 'monthly' | 'yearly';
|
|
25
|
+
interval: number;
|
|
26
|
+
until: string;
|
|
27
|
+
daysOfWeek?: SessionRecurrenceDay[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type ClassSessionTemplateSummary = {
|
|
31
|
+
title: string;
|
|
32
|
+
isRecurring: boolean;
|
|
33
|
+
recurrence?: NormalizedSessionRecurrence | null;
|
|
34
|
+
};
|
|
35
35
|
|
|
36
36
|
@Injectable()
|
|
37
37
|
export class ClassGroupService {
|
|
@@ -133,7 +133,7 @@ export class ClassGroupService {
|
|
|
133
133
|
const pageSize = Math.max(Number(params.pageSize) || 12, 1);
|
|
134
134
|
const skip = (page - 1) * pageSize;
|
|
135
135
|
|
|
136
|
-
const where: any = {};
|
|
136
|
+
const where: any = {};
|
|
137
137
|
|
|
138
138
|
const status = this.normalizeStatus(params.status);
|
|
139
139
|
if (status) {
|
|
@@ -230,33 +230,33 @@ export class ClassGroupService {
|
|
|
230
230
|
};
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
async getById(id: number) {
|
|
234
|
-
const item = await this.prisma.course_class_group.findUnique({
|
|
235
|
-
where: { id },
|
|
236
|
-
include: this.getClassGroupInclude(),
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
if (!item) {
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const sessionTemplateSummary = await this.getClassSessionTemplateSummary(id);
|
|
244
|
-
|
|
245
|
-
return {
|
|
246
|
-
...this.mapClassGroup(item),
|
|
247
|
-
sessionTitle: sessionTemplateSummary?.title ?? null,
|
|
248
|
-
sessionRecurrenceSummary: sessionTemplateSummary
|
|
249
|
-
? {
|
|
250
|
-
...(sessionTemplateSummary.recurrence ?? {}),
|
|
251
|
-
isRecurring: sessionTemplateSummary.isRecurring,
|
|
252
|
-
}
|
|
253
|
-
: null,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async create(dto: CreateClassGroupDto) {
|
|
258
|
-
await this.assertCourseExists(dto.courseId);
|
|
259
|
-
if (dto.instructorId) {
|
|
233
|
+
async getById(id: number) {
|
|
234
|
+
const item = await this.prisma.course_class_group.findUnique({
|
|
235
|
+
where: { id },
|
|
236
|
+
include: this.getClassGroupInclude(),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (!item) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const sessionTemplateSummary = await this.getClassSessionTemplateSummary(id);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
...this.mapClassGroup(item),
|
|
247
|
+
sessionTitle: sessionTemplateSummary?.title ?? null,
|
|
248
|
+
sessionRecurrenceSummary: sessionTemplateSummary
|
|
249
|
+
? {
|
|
250
|
+
...(sessionTemplateSummary.recurrence ?? {}),
|
|
251
|
+
isRecurring: sessionTemplateSummary.isRecurring,
|
|
252
|
+
}
|
|
253
|
+
: null,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async create(dto: CreateClassGroupDto) {
|
|
258
|
+
await this.assertCourseExists(dto.courseId);
|
|
259
|
+
if (dto.instructorId) {
|
|
260
260
|
await this.assertInstructorExists(dto.instructorId);
|
|
261
261
|
}
|
|
262
262
|
this.assertDatesAndTimes({
|
|
@@ -264,68 +264,68 @@ export class ClassGroupService {
|
|
|
264
264
|
endDate: dto.endDate,
|
|
265
265
|
startTime: dto.startTime,
|
|
266
266
|
endTime: dto.endTime,
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
const created = await this.prisma.$transaction(async (tx) => {
|
|
271
|
-
const createdClass = await tx.course_class_group.create({
|
|
272
|
-
data: {
|
|
273
|
-
code: dto.code,
|
|
274
|
-
course_id: dto.courseId,
|
|
275
|
-
instructor_id: dto.instructorId,
|
|
276
|
-
title: dto.title,
|
|
277
|
-
description: dto.description,
|
|
278
|
-
delivery_mode: dto.deliveryMode,
|
|
279
|
-
status: dto.status ?? 'open',
|
|
280
|
-
start_date: new Date(dto.startDate),
|
|
281
|
-
end_date: dto.endDate ? new Date(dto.endDate) : null,
|
|
282
|
-
start_time: dto.startTime,
|
|
283
|
-
end_time: dto.endTime,
|
|
284
|
-
week_days: dto.weekDays,
|
|
285
|
-
capacity: dto.capacity ?? 30,
|
|
286
|
-
location: dto.location,
|
|
287
|
-
virtual_room_url: dto.virtualRoomUrl,
|
|
288
|
-
},
|
|
289
|
-
include: this.getClassGroupInclude(),
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
if (dto.sessionTemplate) {
|
|
293
|
-
await this.syncClassSessionsFromTemplate(
|
|
294
|
-
tx,
|
|
295
|
-
createdClass.id,
|
|
296
|
-
createdClass,
|
|
297
|
-
dto.sessionTemplate,
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return createdClass;
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
return this.mapClassGroup(created);
|
|
305
|
-
} catch (error) {
|
|
306
|
-
this.handlePrismaError(error);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const created = await this.prisma.$transaction(async (tx) => {
|
|
271
|
+
const createdClass = await tx.course_class_group.create({
|
|
272
|
+
data: {
|
|
273
|
+
code: dto.code,
|
|
274
|
+
course_id: dto.courseId,
|
|
275
|
+
instructor_id: dto.instructorId,
|
|
276
|
+
title: dto.title,
|
|
277
|
+
description: dto.description,
|
|
278
|
+
delivery_mode: dto.deliveryMode,
|
|
279
|
+
status: dto.status ?? 'open',
|
|
280
|
+
start_date: new Date(dto.startDate),
|
|
281
|
+
end_date: dto.endDate ? new Date(dto.endDate) : null,
|
|
282
|
+
start_time: dto.startTime,
|
|
283
|
+
end_time: dto.endTime,
|
|
284
|
+
week_days: dto.weekDays,
|
|
285
|
+
capacity: dto.capacity ?? 30,
|
|
286
|
+
location: dto.location,
|
|
287
|
+
virtual_room_url: dto.virtualRoomUrl,
|
|
288
|
+
},
|
|
289
|
+
include: this.getClassGroupInclude(),
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (dto.sessionTemplate) {
|
|
293
|
+
await this.syncClassSessionsFromTemplate(
|
|
294
|
+
tx,
|
|
295
|
+
createdClass.id,
|
|
296
|
+
createdClass,
|
|
297
|
+
dto.sessionTemplate,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return createdClass;
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return this.mapClassGroup(created);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
this.handlePrismaError(error);
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
async update(id: number, dto: UpdateClassGroupDto) {
|
|
311
311
|
const existing = await this.prisma.course_class_group.findUnique({
|
|
312
312
|
where: { id },
|
|
313
|
-
select: {
|
|
314
|
-
id: true,
|
|
315
|
-
course_id: true,
|
|
316
|
-
code: true,
|
|
317
|
-
title: true,
|
|
318
|
-
description: true,
|
|
319
|
-
delivery_mode: true,
|
|
320
|
-
instructor_id: true,
|
|
321
|
-
start_date: true,
|
|
322
|
-
end_date: true,
|
|
323
|
-
start_time: true,
|
|
324
|
-
end_time: true,
|
|
325
|
-
location: true,
|
|
326
|
-
virtual_room_url: true,
|
|
327
|
-
},
|
|
328
|
-
});
|
|
313
|
+
select: {
|
|
314
|
+
id: true,
|
|
315
|
+
course_id: true,
|
|
316
|
+
code: true,
|
|
317
|
+
title: true,
|
|
318
|
+
description: true,
|
|
319
|
+
delivery_mode: true,
|
|
320
|
+
instructor_id: true,
|
|
321
|
+
start_date: true,
|
|
322
|
+
end_date: true,
|
|
323
|
+
start_time: true,
|
|
324
|
+
end_time: true,
|
|
325
|
+
location: true,
|
|
326
|
+
virtual_room_url: true,
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
329
|
|
|
330
330
|
if (!existing) {
|
|
331
331
|
throw new NotFoundException('Class not found');
|
|
@@ -351,198 +351,198 @@ export class ClassGroupService {
|
|
|
351
351
|
|
|
352
352
|
const status =
|
|
353
353
|
dto.status !== undefined ? this.normalizeStatus(dto.status) : undefined;
|
|
354
|
-
const deliveryMode =
|
|
355
|
-
dto.deliveryMode !== undefined
|
|
356
|
-
? this.normalizeDeliveryMode(dto.deliveryMode)
|
|
357
|
-
: undefined;
|
|
358
|
-
|
|
359
|
-
try {
|
|
360
|
-
const updated = await this.prisma.$transaction(async (tx) => {
|
|
361
|
-
const updatedClass = await tx.course_class_group.update({
|
|
362
|
-
where: { id },
|
|
363
|
-
data: {
|
|
364
|
-
...(dto.code !== undefined && { code: dto.code }),
|
|
365
|
-
...(dto.courseId !== undefined && { course_id: dto.courseId }),
|
|
366
|
-
...(dto.instructorId !== undefined && {
|
|
367
|
-
instructor_id: dto.instructorId,
|
|
368
|
-
}),
|
|
369
|
-
...(dto.title !== undefined && { title: dto.title }),
|
|
370
|
-
...(dto.description !== undefined && { description: dto.description }),
|
|
371
|
-
...(deliveryMode !== undefined && { delivery_mode: deliveryMode }),
|
|
372
|
-
...(status !== undefined && { status }),
|
|
373
|
-
...(dto.startDate !== undefined && { start_date: new Date(dto.startDate) }),
|
|
374
|
-
...(dto.endDate !== undefined && {
|
|
375
|
-
end_date: dto.endDate ? new Date(dto.endDate) : null,
|
|
376
|
-
}),
|
|
377
|
-
...(dto.startTime !== undefined && { start_time: dto.startTime }),
|
|
378
|
-
...(dto.endTime !== undefined && { end_time: dto.endTime }),
|
|
379
|
-
...(dto.weekDays !== undefined && { week_days: dto.weekDays }),
|
|
380
|
-
...(dto.capacity !== undefined && { capacity: dto.capacity }),
|
|
381
|
-
...(dto.location !== undefined && { location: dto.location }),
|
|
382
|
-
...(dto.virtualRoomUrl !== undefined && {
|
|
383
|
-
virtual_room_url: dto.virtualRoomUrl,
|
|
384
|
-
}),
|
|
385
|
-
},
|
|
386
|
-
include: this.getClassGroupInclude(),
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
if (dto.sessionTemplate) {
|
|
390
|
-
await this.syncClassSessionsFromTemplate(
|
|
391
|
-
tx,
|
|
392
|
-
id,
|
|
393
|
-
updatedClass,
|
|
394
|
-
dto.sessionTemplate,
|
|
395
|
-
{ futureOnly: true },
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return updatedClass;
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
return this.mapClassGroup(updated);
|
|
403
|
-
} catch (error) {
|
|
404
|
-
this.handlePrismaError(error);
|
|
354
|
+
const deliveryMode =
|
|
355
|
+
dto.deliveryMode !== undefined
|
|
356
|
+
? this.normalizeDeliveryMode(dto.deliveryMode)
|
|
357
|
+
: undefined;
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const updated = await this.prisma.$transaction(async (tx) => {
|
|
361
|
+
const updatedClass = await tx.course_class_group.update({
|
|
362
|
+
where: { id },
|
|
363
|
+
data: {
|
|
364
|
+
...(dto.code !== undefined && { code: dto.code }),
|
|
365
|
+
...(dto.courseId !== undefined && { course_id: dto.courseId }),
|
|
366
|
+
...(dto.instructorId !== undefined && {
|
|
367
|
+
instructor_id: dto.instructorId,
|
|
368
|
+
}),
|
|
369
|
+
...(dto.title !== undefined && { title: dto.title }),
|
|
370
|
+
...(dto.description !== undefined && { description: dto.description }),
|
|
371
|
+
...(deliveryMode !== undefined && { delivery_mode: deliveryMode }),
|
|
372
|
+
...(status !== undefined && { status }),
|
|
373
|
+
...(dto.startDate !== undefined && { start_date: new Date(dto.startDate) }),
|
|
374
|
+
...(dto.endDate !== undefined && {
|
|
375
|
+
end_date: dto.endDate ? new Date(dto.endDate) : null,
|
|
376
|
+
}),
|
|
377
|
+
...(dto.startTime !== undefined && { start_time: dto.startTime }),
|
|
378
|
+
...(dto.endTime !== undefined && { end_time: dto.endTime }),
|
|
379
|
+
...(dto.weekDays !== undefined && { week_days: dto.weekDays }),
|
|
380
|
+
...(dto.capacity !== undefined && { capacity: dto.capacity }),
|
|
381
|
+
...(dto.location !== undefined && { location: dto.location }),
|
|
382
|
+
...(dto.virtualRoomUrl !== undefined && {
|
|
383
|
+
virtual_room_url: dto.virtualRoomUrl,
|
|
384
|
+
}),
|
|
385
|
+
},
|
|
386
|
+
include: this.getClassGroupInclude(),
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
if (dto.sessionTemplate) {
|
|
390
|
+
await this.syncClassSessionsFromTemplate(
|
|
391
|
+
tx,
|
|
392
|
+
id,
|
|
393
|
+
updatedClass,
|
|
394
|
+
dto.sessionTemplate,
|
|
395
|
+
{ futureOnly: true },
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return updatedClass;
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return this.mapClassGroup(updated);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
this.handlePrismaError(error);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async remove(id: number) {
|
|
409
|
+
await this.prisma.course_class_group.delete({ where: { id } });
|
|
410
|
+
return { success: true };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private async getClassSessionTemplateSummary(
|
|
414
|
+
classGroupId: number,
|
|
415
|
+
): Promise<ClassSessionTemplateSummary | null> {
|
|
416
|
+
const today = this.getSessionMutationFloorDate();
|
|
417
|
+
const session =
|
|
418
|
+
(await this.prisma.course_class_session.findFirst({
|
|
419
|
+
where: {
|
|
420
|
+
course_class_group_id: classGroupId,
|
|
421
|
+
session_date: { gte: today },
|
|
422
|
+
status: { notIn: ['completed', 'cancelled'] },
|
|
423
|
+
},
|
|
424
|
+
orderBy: [{ session_date: 'asc' }, { occurrence_index: 'asc' }],
|
|
425
|
+
select: {
|
|
426
|
+
title: true,
|
|
427
|
+
recurrence_rule: true,
|
|
428
|
+
recurrence_id: true,
|
|
429
|
+
},
|
|
430
|
+
})) ??
|
|
431
|
+
(await this.prisma.course_class_session.findFirst({
|
|
432
|
+
where: { course_class_group_id: classGroupId },
|
|
433
|
+
orderBy: [{ session_date: 'asc' }, { occurrence_index: 'asc' }],
|
|
434
|
+
select: {
|
|
435
|
+
title: true,
|
|
436
|
+
recurrence_rule: true,
|
|
437
|
+
recurrence_id: true,
|
|
438
|
+
},
|
|
439
|
+
}));
|
|
440
|
+
|
|
441
|
+
if (!session) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
title: session.title,
|
|
447
|
+
recurrence: this.parseRecurrenceRule(session.recurrence_rule),
|
|
448
|
+
isRecurring: Boolean(session.recurrence_id),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private getSessionMutationFloorDate() {
|
|
453
|
+
return this.normalizeSessionDate(new Date());
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private buildClassSessionDto(
|
|
457
|
+
classGroup: any,
|
|
458
|
+
sessionTemplate: ClassGroupSessionTemplateDto,
|
|
459
|
+
): CreateSessionDto {
|
|
460
|
+
if (!classGroup.start_date) {
|
|
461
|
+
throw new BadRequestException('Class start date is required to generate sessions');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (!classGroup.start_time || !classGroup.end_time) {
|
|
465
|
+
throw new BadRequestException('Class start and end times are required to generate sessions');
|
|
405
466
|
}
|
|
467
|
+
|
|
468
|
+
const deliveryMode = classGroup.delivery_mode;
|
|
469
|
+
const isOnline = deliveryMode === 'online';
|
|
470
|
+
const isHybrid = deliveryMode === 'hybrid';
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
title: sessionTemplate.title.trim(),
|
|
474
|
+
description: sessionTemplate.description ?? classGroup.description ?? undefined,
|
|
475
|
+
sessionDate: this.toDateKey(this.normalizeSessionDate(classGroup.start_date)),
|
|
476
|
+
startTime: classGroup.start_time,
|
|
477
|
+
endTime: classGroup.end_time,
|
|
478
|
+
location:
|
|
479
|
+
isOnline && !isHybrid
|
|
480
|
+
? undefined
|
|
481
|
+
: sessionTemplate.location ?? classGroup.location ?? undefined,
|
|
482
|
+
meetingUrl:
|
|
483
|
+
!isOnline && !isHybrid
|
|
484
|
+
? undefined
|
|
485
|
+
: sessionTemplate.meetingUrl ?? classGroup.virtual_room_url ?? undefined,
|
|
486
|
+
color: sessionTemplate.color ?? undefined,
|
|
487
|
+
status: 'scheduled',
|
|
488
|
+
instructorId: classGroup.instructor_id ?? undefined,
|
|
489
|
+
recurrence: sessionTemplate.recurrence
|
|
490
|
+
? this.toSessionRecurrenceDto(sessionTemplate.recurrence)
|
|
491
|
+
: undefined,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private toSessionRecurrenceDto(
|
|
496
|
+
recurrence: ClassGroupSessionRecurrenceDto,
|
|
497
|
+
): SessionRecurrenceDto {
|
|
498
|
+
return {
|
|
499
|
+
frequency: recurrence.frequency,
|
|
500
|
+
interval: recurrence.interval,
|
|
501
|
+
until: recurrence.until,
|
|
502
|
+
daysOfWeek: recurrence.daysOfWeek,
|
|
503
|
+
};
|
|
406
504
|
}
|
|
407
505
|
|
|
408
|
-
async
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
(
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
isRecurring: Boolean(session.recurrence_id),
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
private getSessionMutationFloorDate() {
|
|
453
|
-
return this.normalizeSessionDate(new Date());
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
private buildClassSessionDto(
|
|
457
|
-
classGroup: any,
|
|
458
|
-
sessionTemplate: ClassGroupSessionTemplateDto,
|
|
459
|
-
): CreateSessionDto {
|
|
460
|
-
if (!classGroup.start_date) {
|
|
461
|
-
throw new BadRequestException('Class start date is required to generate sessions');
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (!classGroup.start_time || !classGroup.end_time) {
|
|
465
|
-
throw new BadRequestException('Class start and end times are required to generate sessions');
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const deliveryMode = classGroup.delivery_mode;
|
|
469
|
-
const isOnline = deliveryMode === 'online';
|
|
470
|
-
const isHybrid = deliveryMode === 'hybrid';
|
|
471
|
-
|
|
472
|
-
return {
|
|
473
|
-
title: sessionTemplate.title.trim(),
|
|
474
|
-
description: sessionTemplate.description ?? classGroup.description ?? undefined,
|
|
475
|
-
sessionDate: this.toDateKey(this.normalizeSessionDate(classGroup.start_date)),
|
|
476
|
-
startTime: classGroup.start_time,
|
|
477
|
-
endTime: classGroup.end_time,
|
|
478
|
-
location:
|
|
479
|
-
isOnline && !isHybrid
|
|
480
|
-
? undefined
|
|
481
|
-
: sessionTemplate.location ?? classGroup.location ?? undefined,
|
|
482
|
-
meetingUrl:
|
|
483
|
-
!isOnline && !isHybrid
|
|
484
|
-
? undefined
|
|
485
|
-
: sessionTemplate.meetingUrl ?? classGroup.virtual_room_url ?? undefined,
|
|
486
|
-
color: sessionTemplate.color ?? undefined,
|
|
487
|
-
status: 'scheduled',
|
|
488
|
-
instructorId: classGroup.instructor_id ?? undefined,
|
|
489
|
-
recurrence: sessionTemplate.recurrence
|
|
490
|
-
? this.toSessionRecurrenceDto(sessionTemplate.recurrence)
|
|
491
|
-
: undefined,
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
private toSessionRecurrenceDto(
|
|
496
|
-
recurrence: ClassGroupSessionRecurrenceDto,
|
|
497
|
-
): SessionRecurrenceDto {
|
|
498
|
-
return {
|
|
499
|
-
frequency: recurrence.frequency,
|
|
500
|
-
interval: recurrence.interval,
|
|
501
|
-
until: recurrence.until,
|
|
502
|
-
daysOfWeek: recurrence.daysOfWeek,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
private async syncClassSessionsFromTemplate(
|
|
507
|
-
tx: Prisma.TransactionClient,
|
|
508
|
-
classGroupId: number,
|
|
509
|
-
classGroup: any,
|
|
510
|
-
sessionTemplate: ClassGroupSessionTemplateDto,
|
|
511
|
-
options?: { futureOnly?: boolean },
|
|
512
|
-
) {
|
|
513
|
-
const createDto = this.buildClassSessionDto(classGroup, sessionTemplate);
|
|
514
|
-
const mutableWhere: any = {
|
|
515
|
-
course_class_group_id: classGroupId,
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
if (options?.futureOnly) {
|
|
519
|
-
mutableWhere.session_date = { gte: this.getSessionMutationFloorDate() };
|
|
520
|
-
mutableWhere.status = { notIn: ['completed', 'cancelled'] };
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const mutableSessions = await tx.course_class_session.findMany({
|
|
524
|
-
where: mutableWhere,
|
|
525
|
-
select: { id: true },
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
if (mutableSessions.length > 0) {
|
|
529
|
-
const sessionIds = mutableSessions.map((session) => session.id);
|
|
530
|
-
|
|
531
|
-
await tx.course_class_attendance.deleteMany({
|
|
532
|
-
where: { course_class_session_id: { in: sessionIds } },
|
|
533
|
-
});
|
|
534
|
-
await tx.course_class_session_instructor.deleteMany({
|
|
535
|
-
where: { course_class_session_id: { in: sessionIds } },
|
|
536
|
-
});
|
|
537
|
-
await tx.course_class_session.deleteMany({
|
|
538
|
-
where: { id: { in: sessionIds } },
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
await this.createSessionSeriesInTransaction(tx, classGroupId, createDto, {
|
|
543
|
-
minDate: options?.futureOnly ? this.getSessionMutationFloorDate() : undefined,
|
|
544
|
-
});
|
|
545
|
-
}
|
|
506
|
+
private async syncClassSessionsFromTemplate(
|
|
507
|
+
tx: Prisma.TransactionClient,
|
|
508
|
+
classGroupId: number,
|
|
509
|
+
classGroup: any,
|
|
510
|
+
sessionTemplate: ClassGroupSessionTemplateDto,
|
|
511
|
+
options?: { futureOnly?: boolean },
|
|
512
|
+
) {
|
|
513
|
+
const createDto = this.buildClassSessionDto(classGroup, sessionTemplate);
|
|
514
|
+
const mutableWhere: any = {
|
|
515
|
+
course_class_group_id: classGroupId,
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
if (options?.futureOnly) {
|
|
519
|
+
mutableWhere.session_date = { gte: this.getSessionMutationFloorDate() };
|
|
520
|
+
mutableWhere.status = { notIn: ['completed', 'cancelled'] };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const mutableSessions = await tx.course_class_session.findMany({
|
|
524
|
+
where: mutableWhere,
|
|
525
|
+
select: { id: true },
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
if (mutableSessions.length > 0) {
|
|
529
|
+
const sessionIds = mutableSessions.map((session) => session.id);
|
|
530
|
+
|
|
531
|
+
await tx.course_class_attendance.deleteMany({
|
|
532
|
+
where: { course_class_session_id: { in: sessionIds } },
|
|
533
|
+
});
|
|
534
|
+
await tx.course_class_session_instructor.deleteMany({
|
|
535
|
+
where: { course_class_session_id: { in: sessionIds } },
|
|
536
|
+
});
|
|
537
|
+
await tx.course_class_session.deleteMany({
|
|
538
|
+
where: { id: { in: sessionIds } },
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
await this.createSessionSeriesInTransaction(tx, classGroupId, createDto, {
|
|
543
|
+
minDate: options?.futureOnly ? this.getSessionMutationFloorDate() : undefined,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
546
|
|
|
547
547
|
private mapClassGroup(item: any) {
|
|
548
548
|
const instructorName =
|
|
@@ -1148,117 +1148,117 @@ export class ClassGroupService {
|
|
|
1148
1148
|
return sessions.map((s) => this.mapSession(s));
|
|
1149
1149
|
}
|
|
1150
1150
|
|
|
1151
|
-
async createSession(classGroupId: number, dto: CreateSessionDto) {
|
|
1152
|
-
const group = await this.prisma.course_class_group.findUnique({
|
|
1153
|
-
where: { id: classGroupId },
|
|
1154
|
-
select: { id: true },
|
|
1155
|
-
});
|
|
1151
|
+
async createSession(classGroupId: number, dto: CreateSessionDto) {
|
|
1152
|
+
const group = await this.prisma.course_class_group.findUnique({
|
|
1153
|
+
where: { id: classGroupId },
|
|
1154
|
+
select: { id: true },
|
|
1155
|
+
});
|
|
1156
1156
|
if (!group) throw new NotFoundException('Class not found');
|
|
1157
1157
|
|
|
1158
|
-
if (dto.instructorId) {
|
|
1159
|
-
await this.assertInstructorExists(dto.instructorId);
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
const created = await this.prisma.$transaction((tx) =>
|
|
1163
|
-
this.createSessionSeriesInTransaction(tx, classGroupId, dto),
|
|
1164
|
-
);
|
|
1165
|
-
|
|
1166
|
-
if (!created) {
|
|
1167
|
-
throw new BadRequestException('No sessions were generated for the provided recurrence');
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
return this.mapSession(created);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
private async createSessionSeriesInTransaction(
|
|
1174
|
-
tx: Prisma.TransactionClient,
|
|
1175
|
-
classGroupId: number,
|
|
1176
|
-
dto: CreateSessionDto,
|
|
1177
|
-
options?: { minDate?: Date },
|
|
1178
|
-
) {
|
|
1179
|
-
const normalizedRecurrence = dto.recurrence
|
|
1180
|
-
? this.normalizeSessionRecurrence(dto.recurrence, dto.sessionDate)
|
|
1181
|
-
: null;
|
|
1182
|
-
const fullOccurrenceDates = normalizedRecurrence
|
|
1183
|
-
? this.buildRecurringSessionDates(dto.sessionDate, normalizedRecurrence)
|
|
1184
|
-
: [this.toSessionDate(dto.sessionDate)];
|
|
1185
|
-
const occurrenceDates = options?.minDate
|
|
1186
|
-
? fullOccurrenceDates.filter(
|
|
1187
|
-
(date) =>
|
|
1188
|
-
this.normalizeSessionDate(date).getTime() >=
|
|
1189
|
-
this.normalizeSessionDate(options.minDate!).getTime(),
|
|
1190
|
-
)
|
|
1191
|
-
: fullOccurrenceDates;
|
|
1192
|
-
|
|
1193
|
-
if (occurrenceDates.length === 0) {
|
|
1194
|
-
return null;
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
const recurrenceId = normalizedRecurrence ? randomUUID() : null;
|
|
1198
|
-
const recurrenceRule = normalizedRecurrence
|
|
1199
|
-
? JSON.stringify(normalizedRecurrence)
|
|
1200
|
-
: null;
|
|
1201
|
-
|
|
1202
|
-
const first = await tx.course_class_session.create({
|
|
1203
|
-
data: {
|
|
1204
|
-
course_class_group_id: classGroupId,
|
|
1205
|
-
title: dto.title,
|
|
1206
|
-
description: dto.description ?? null,
|
|
1207
|
-
session_date: occurrenceDates[0],
|
|
1208
|
-
start_time: dto.startTime,
|
|
1209
|
-
end_time: dto.endTime,
|
|
1210
|
-
location: dto.location ?? null,
|
|
1211
|
-
meeting_url: dto.meetingUrl ?? null,
|
|
1212
|
-
color: dto.color ?? null,
|
|
1213
|
-
status: dto.status ?? 'scheduled',
|
|
1214
|
-
recurrence_id: recurrenceId,
|
|
1215
|
-
recurrence_rule: recurrenceRule,
|
|
1216
|
-
occurrence_index: normalizedRecurrence ? 0 : null,
|
|
1217
|
-
is_exception: false,
|
|
1218
|
-
...(dto.instructorId && {
|
|
1219
|
-
course_class_session_instructor: {
|
|
1220
|
-
create: {
|
|
1221
|
-
instructor_id: dto.instructorId,
|
|
1222
|
-
role: 'lead',
|
|
1223
|
-
},
|
|
1224
|
-
},
|
|
1225
|
-
}),
|
|
1226
|
-
},
|
|
1227
|
-
include: this.getSessionInclude(),
|
|
1228
|
-
});
|
|
1229
|
-
|
|
1230
|
-
for (let index = 1; index < occurrenceDates.length; index += 1) {
|
|
1231
|
-
await tx.course_class_session.create({
|
|
1232
|
-
data: {
|
|
1233
|
-
course_class_group_id: classGroupId,
|
|
1234
|
-
title: dto.title,
|
|
1235
|
-
description: dto.description ?? null,
|
|
1236
|
-
session_date: occurrenceDates[index],
|
|
1237
|
-
start_time: dto.startTime,
|
|
1238
|
-
end_time: dto.endTime,
|
|
1239
|
-
location: dto.location ?? null,
|
|
1240
|
-
meeting_url: dto.meetingUrl ?? null,
|
|
1241
|
-
color: dto.color ?? null,
|
|
1242
|
-
status: dto.status ?? 'scheduled',
|
|
1243
|
-
recurrence_id: recurrenceId,
|
|
1244
|
-
recurrence_rule: recurrenceRule,
|
|
1245
|
-
occurrence_index: normalizedRecurrence ? index : null,
|
|
1246
|
-
is_exception: false,
|
|
1247
|
-
parent_session_id: first.id,
|
|
1248
|
-
...(dto.instructorId && {
|
|
1249
|
-
course_class_session_instructor: {
|
|
1250
|
-
create: {
|
|
1251
|
-
instructor_id: dto.instructorId,
|
|
1252
|
-
role: 'lead',
|
|
1253
|
-
},
|
|
1254
|
-
},
|
|
1255
|
-
}),
|
|
1256
|
-
},
|
|
1257
|
-
});
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
return first;
|
|
1261
|
-
}
|
|
1158
|
+
if (dto.instructorId) {
|
|
1159
|
+
await this.assertInstructorExists(dto.instructorId);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const created = await this.prisma.$transaction((tx) =>
|
|
1163
|
+
this.createSessionSeriesInTransaction(tx, classGroupId, dto),
|
|
1164
|
+
);
|
|
1165
|
+
|
|
1166
|
+
if (!created) {
|
|
1167
|
+
throw new BadRequestException('No sessions were generated for the provided recurrence');
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
return this.mapSession(created);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
private async createSessionSeriesInTransaction(
|
|
1174
|
+
tx: Prisma.TransactionClient,
|
|
1175
|
+
classGroupId: number,
|
|
1176
|
+
dto: CreateSessionDto,
|
|
1177
|
+
options?: { minDate?: Date },
|
|
1178
|
+
) {
|
|
1179
|
+
const normalizedRecurrence = dto.recurrence
|
|
1180
|
+
? this.normalizeSessionRecurrence(dto.recurrence, dto.sessionDate)
|
|
1181
|
+
: null;
|
|
1182
|
+
const fullOccurrenceDates = normalizedRecurrence
|
|
1183
|
+
? this.buildRecurringSessionDates(dto.sessionDate, normalizedRecurrence)
|
|
1184
|
+
: [this.toSessionDate(dto.sessionDate)];
|
|
1185
|
+
const occurrenceDates = options?.minDate
|
|
1186
|
+
? fullOccurrenceDates.filter(
|
|
1187
|
+
(date) =>
|
|
1188
|
+
this.normalizeSessionDate(date).getTime() >=
|
|
1189
|
+
this.normalizeSessionDate(options.minDate!).getTime(),
|
|
1190
|
+
)
|
|
1191
|
+
: fullOccurrenceDates;
|
|
1192
|
+
|
|
1193
|
+
if (occurrenceDates.length === 0) {
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const recurrenceId = normalizedRecurrence ? randomUUID() : null;
|
|
1198
|
+
const recurrenceRule = normalizedRecurrence
|
|
1199
|
+
? JSON.stringify(normalizedRecurrence)
|
|
1200
|
+
: null;
|
|
1201
|
+
|
|
1202
|
+
const first = await tx.course_class_session.create({
|
|
1203
|
+
data: {
|
|
1204
|
+
course_class_group_id: classGroupId,
|
|
1205
|
+
title: dto.title,
|
|
1206
|
+
description: dto.description ?? null,
|
|
1207
|
+
session_date: occurrenceDates[0],
|
|
1208
|
+
start_time: dto.startTime,
|
|
1209
|
+
end_time: dto.endTime,
|
|
1210
|
+
location: dto.location ?? null,
|
|
1211
|
+
meeting_url: dto.meetingUrl ?? null,
|
|
1212
|
+
color: dto.color ?? null,
|
|
1213
|
+
status: dto.status ?? 'scheduled',
|
|
1214
|
+
recurrence_id: recurrenceId,
|
|
1215
|
+
recurrence_rule: recurrenceRule,
|
|
1216
|
+
occurrence_index: normalizedRecurrence ? 0 : null,
|
|
1217
|
+
is_exception: false,
|
|
1218
|
+
...(dto.instructorId && {
|
|
1219
|
+
course_class_session_instructor: {
|
|
1220
|
+
create: {
|
|
1221
|
+
instructor_id: dto.instructorId,
|
|
1222
|
+
role: 'lead',
|
|
1223
|
+
},
|
|
1224
|
+
},
|
|
1225
|
+
}),
|
|
1226
|
+
},
|
|
1227
|
+
include: this.getSessionInclude(),
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
for (let index = 1; index < occurrenceDates.length; index += 1) {
|
|
1231
|
+
await tx.course_class_session.create({
|
|
1232
|
+
data: {
|
|
1233
|
+
course_class_group_id: classGroupId,
|
|
1234
|
+
title: dto.title,
|
|
1235
|
+
description: dto.description ?? null,
|
|
1236
|
+
session_date: occurrenceDates[index],
|
|
1237
|
+
start_time: dto.startTime,
|
|
1238
|
+
end_time: dto.endTime,
|
|
1239
|
+
location: dto.location ?? null,
|
|
1240
|
+
meeting_url: dto.meetingUrl ?? null,
|
|
1241
|
+
color: dto.color ?? null,
|
|
1242
|
+
status: dto.status ?? 'scheduled',
|
|
1243
|
+
recurrence_id: recurrenceId,
|
|
1244
|
+
recurrence_rule: recurrenceRule,
|
|
1245
|
+
occurrence_index: normalizedRecurrence ? index : null,
|
|
1246
|
+
is_exception: false,
|
|
1247
|
+
parent_session_id: first.id,
|
|
1248
|
+
...(dto.instructorId && {
|
|
1249
|
+
course_class_session_instructor: {
|
|
1250
|
+
create: {
|
|
1251
|
+
instructor_id: dto.instructorId,
|
|
1252
|
+
role: 'lead',
|
|
1253
|
+
},
|
|
1254
|
+
},
|
|
1255
|
+
}),
|
|
1256
|
+
},
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
return first;
|
|
1261
|
+
}
|
|
1262
1262
|
|
|
1263
1263
|
async updateSession(
|
|
1264
1264
|
classGroupId: number,
|
|
@@ -1791,8 +1791,8 @@ export class ClassGroupService {
|
|
|
1791
1791
|
|
|
1792
1792
|
private handlePrismaError(error: unknown): never {
|
|
1793
1793
|
if (
|
|
1794
|
-
error instanceof PrismaClientKnownRequestError &&
|
|
1795
|
-
error.code === 'P2002'
|
|
1794
|
+
error instanceof PrismaClientKnownRequestError &&
|
|
1795
|
+
error.code === 'P2002'
|
|
1796
1796
|
) {
|
|
1797
1797
|
throw new ConflictException('Class code already exists');
|
|
1798
1798
|
}
|