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 }, }) }, }), })) } }