Files
epess-web-backend/src/Schedule/schedule.schema.ts
2024-11-28 15:27:41 +07:00

395 lines
12 KiB
TypeScript

import { Inject, Injectable, Logger } from '@nestjs/common'
import {
Pothos,
PothosRef,
PothosSchema,
SchemaBuilderToken,
} from '@smatch-corp/nestjs-pothos'
import { Builder } from '../Graphql/graphql.builder'
import { PrismaService } from '../Prisma/prisma.service'
import { ScheduleDateStatus, ScheduleStatus } from '@prisma/client'
import { ScheduleService } from './schedule.service'
import { AppConfigService } from '../AppConfig/appconfig.service'
import { ScheduleConfigType } from './schedule'
import { DateTimeUtils } from 'src/common/utils/datetime.utils'
@Injectable()
export class ScheduleSchema extends PothosSchema {
constructor(
@Inject(SchemaBuilderToken) private readonly builder: Builder,
private readonly prisma: PrismaService,
private readonly scheduleService: ScheduleService,
private readonly appConfigService: AppConfigService,
) {
super()
}
@PothosRef()
schedule() {
return this.builder.prismaObject('Schedule', {
description: 'A schedule in the system.',
fields: (t) => ({
id: t.exposeID('id', {
description: 'The ID of the schedule.',
}),
customerId: t.exposeID('customerId', {
description: 'The ID of the customer the schedule belongs to.',
nullable: true,
}),
managedServiceId: t.exposeID('managedServiceId', {
description: 'The ID of the managed service the schedule belongs to.',
nullable: false,
}),
orderId: t.exposeID('orderId', {
description: 'The ID of the order the schedule belongs to.',
nullable: true,
}),
order: t.relation('Order', {
description: 'The order that belongs to orderId.',
nullable: true,
}),
scheduleStart: t.expose('scheduleStart', {
type: 'DateTime',
nullable: false,
}),
scheduleEnd: t.expose('scheduleEnd', {
type: 'DateTime',
nullable: false,
}),
slots: t.exposeIntList('slots', {
nullable: false,
}),
daysOfWeek: t.exposeIntList('daysOfWeek', {
nullable: false,
}),
dates: t.relation('dates', {
description: 'The dates of the schedule.',
}),
status: t.expose('status', {
type: ScheduleStatus,
nullable: false,
}),
managedService: t.relation('managedService', {
description: 'The managed service the schedule belongs to.',
nullable: false,
}),
}),
})
}
@PothosRef()
scheduleConnection() {
return this.builder.simpleObject('ScheduleConnection', {
fields: (t) => ({
totalCount: t.int({
nullable: true,
}),
schedules: t.field({
type: [this.schedule()],
}),
}),
})
}
@PothosRef()
scheduleSlot() {
return this.builder.simpleObject('ScheduleSlot', {
fields: (t) => ({
slot: t.string({}),
start: t.string({}),
end: t.string({}),
dayOfWeek: t.int({}),
}),
})
}
@PothosRef()
previewSchedule() {
return this.builder.simpleObject('PreviewSchedule', {
fields: (t) => ({
totalSlots: t.int(),
slots: t.field({
type: [this.scheduleSlot()],
}),
}),
})
}
@PothosRef()
scheduleDate() {
return this.builder.prismaObject('ScheduleDate', {
description: 'A schedule date in the system.',
fields: (t) => ({
id: t.exposeID('id', {
description: 'The ID of the schedule date.',
}),
scheduleId: t.exposeID('scheduleId', {
description: 'The ID of the schedule the schedule date belongs to.',
}),
start: t.expose('start', {
type: 'DateTime',
nullable: false,
}),
end: t.expose('end', {
type: 'DateTime',
nullable: false,
}),
status: t.expose('status', {
type: ScheduleDateStatus,
nullable: false,
}),
dayOfWeek: t.exposeInt('dayOfWeek', {
nullable: false,
}),
slot: t.exposeInt('slot', {
nullable: false,
}),
serviceId: t.exposeID('serviceId', {
nullable: false,
}),
orderId: t.exposeID('orderId', {
nullable: true,
}),
participantIds: t.exposeStringList('participantIds', {
nullable: false,
}),
maxParticipants: t.exposeInt('maxParticipants', {
nullable: false,
}),
lateStart: t.expose('lateStart', {
type: 'DateTime',
nullable: true,
}),
collaborationSession: t.relation('CollaborationSession', {
description: 'The collaboration session of the schedule date.',
nullable: true,
}),
schedule: t.relation('schedule', {
description: 'The schedule the schedule date belongs to.',
}),
}),
})
}
@PothosRef()
scheduleConfigInput() {
return this.builder.inputType('ScheduleConfigInput', {
description: 'A schedule config in the system.',
fields: (t) => ({
midDayBreakTimeStart: t.string({
required: true,
}),
midDayBreakTimeEnd: t.string({
required: true,
}),
slotDuration: t.string({
required: true,
}),
slotBreakDuration: t.string({
required: true,
}),
dayStartTime: t.string({
required: true,
}),
dayEndTime: t.string({
required: true,
}),
}),
})
}
@PothosRef()
scheduleConfigInputForCenter() {
return this.builder.inputType('ScheduleConfigInputForCenter', {
fields: (t) => ({
startDate: t.string({
required: true,
}),
endDate: t.string({
required: true,
}),
slots: t.intList({
required: true,
}),
days: t.intList({
required: true,
}),
}),
})
}
@Pothos()
init(): void {
this.builder.queryFields((t) => ({
schedule: t.prismaField({
type: this.schedule(),
description: 'Retrieve a single schedule by its unique identifier.',
args: this.builder.generator.findUniqueArgs('Schedule'),
resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.schedule.findUnique({
...query,
where: args.where,
})
},
}),
schedules: t.prismaField({
type: [this.schedule()],
args: this.builder.generator.findManyArgs('Schedule'),
description:
'Retrieve a list of schedules with optional filtering, ordering, and pagination.',
resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.schedule.findMany({
...query,
skip: args.skip ?? undefined,
take: args.take ?? undefined,
orderBy: args.orderBy ?? undefined,
where: args.filter ?? undefined,
})
},
}),
centerPreviewSchedule: t.field({
type: this.previewSchedule(),
description: 'Preview a schedule for center mentor.',
args: {
scheduleConfigInput: t.arg({
type: this.scheduleConfigInputForCenter(),
required: true,
}),
},
resolve: async (_parent, args, _context, _info) => {
return await this.scheduleService.createSchedulePreviewForCenter(
args.scheduleConfigInput,
)
},
}),
adminPreviewSchedule: t.field({
type: this.previewSchedule(),
description: 'Preview a schedule for admin.',
args: {
scheduleConfig: t.arg({
type: this.scheduleConfigInput(),
}),
},
resolve: async (_parent, args, _context, _info) => {
// if no scheduleConfig, use default config
if (!args.scheduleConfig) {
args.scheduleConfig = (
await this.appConfigService.getVisibleConfigs()
).reduce((acc, curr) => {
// @ts-ignore
acc[curr.key] = curr.value
return acc
}, {} as ScheduleConfigType)
}
return await this.scheduleService.createSchedulePreviewForSingleDay(
args.scheduleConfig,
)
},
}),
}))
/* overlapping case
46836288-bb2c-4da6-892b-a559a480cbf8,e9be51fd-2382-4e43-9988-74e76fde4b56,2024-11-22 00:00:00.000,2024-11-02 00:00:00.000,UNPUBLISHED,,"{3,5}",,"{2,4}"
d72a864e-2f41-45ab-9c9b-bf0512a31883,e9be51fd-2382-4e43-9988-74e76fde4b56,2024-11-22 00:00:00.000,2024-11-02 00:00:00.000,UNPUBLISHED,,"{3,5}",,"{2,4}"
*/
this.builder.mutationFields((t) => ({
// Mutations
createSchedule: t.prismaField({
type: this.schedule(),
description: 'Create a new schedule.',
args: {
schedule: t.arg({
type: this.builder.generator.getCreateInput('Schedule', [
'id',
'status',
'customerId',
'orderId',
'dates',
]),
required: true,
}),
},
resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) {
throw new Error('Cannot create schedule in subscription')
}
Logger.log('args.schedule', args.schedule)
// generate preview and check if there is any overlapping with other schedules date in same service
const previewSchedule =
await this.scheduleService.createSchedulePreviewForCenter({
startDate: args.schedule.scheduleStart as string,
endDate: args.schedule.scheduleEnd as string,
slots: args.schedule.slots as number[],
days: args.schedule.daysOfWeek as number[],
})
const existingScheduleDates = await this.prisma.scheduleDate.findMany(
{
where: {
serviceId: args.schedule.managedService.connect?.id,
},
},
)
// check if there is any overlapping with existing schedule dates in same service using DateTimeUtils
const isOverlapping = DateTimeUtils.isOverlaps(
previewSchedule.slots.map((slot) => ({
start: DateTimeUtils.fromIsoString(slot.start),
end: DateTimeUtils.fromIsoString(slot.end),
})),
existingScheduleDates.map((date) => ({
start: DateTimeUtils.fromDate(date.start),
end: DateTimeUtils.fromDate(date.end),
})),
)
if (isOverlapping) {
Logger.error('Overlapping schedule', 'ScheduleSchema')
throw new Error('Overlapping schedule')
}
const schedule = await this.prisma.schedule.create({
...query,
data: args.schedule,
})
// generate schedule dates based on data and config
const scheduleDates =
await this.scheduleService.generateScheduleDates(schedule)
// update schedule with schedule dates
return await this.prisma.schedule.update({
...query,
where: { id: schedule.id },
data: {
dates: {
connect: scheduleDates.map((date) => ({ id: date.id })),
},
},
})
},
}),
updateScheduleStatus: t.prismaField({
type: this.schedule(),
description: 'Update a schedule status.',
args: {
scheduleId: t.arg({
type: 'String',
required: true,
}),
status: t.arg({
type: ScheduleStatus,
required: true,
}),
},
resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.schedule.update({
...query,
where: { id: args.scheduleId },
data: { status: args.status },
})
},
}),
}))
}
}