From a09785ec719b71601c9667b8f5ff7cbe9189f4d8 Mon Sep 17 00:00:00 2001 From: Ly Tuan Kiet Date: Wed, 30 Oct 2024 17:56:19 +0700 Subject: [PATCH] update preview date --- package-lock.json | 8 ++ package.json | 11 +- src/AppConfig/appconfig.constant.ts | 8 +- src/AppConfig/appconfig.service.ts | 3 + src/Schedule/schedule.module.ts | 5 +- src/Schedule/schedule.schema.ts | 80 ++++++++++++ src/Schedule/schedule.service.ts | 184 +++++++++++++++++++++++++++- src/Service/service.schema.ts | 71 +++++++++-- 8 files changed, 347 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index c3af5bc..b6ad627 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,6 +77,7 @@ "@types/bcryptjs": "^2.4.6", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/lodash": "^4.17.13", "@types/node": "^20.3.1", "@types/nodemailer": "^6.4.16", "@types/passport-jwt": "^4.0.1", @@ -5534,6 +5535,13 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", diff --git a/package.json b/package.json index 8667253..f921d3c 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "@types/bcryptjs": "^2.4.6", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/lodash": "^4.17.13", "@types/node": "^20.3.1", "@types/nodemailer": "^6.4.16", "@types/passport-jwt": "^4.0.1", @@ -123,13 +124,19 @@ "ws": "^8.18.0" }, "jest": { - "moduleFileExtensions": ["js", "json", "ts"], + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, - "collectCoverageFrom": ["**/*.(t|j)s"], + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], "coverageDirectory": "../coverage", "testEnvironment": "node" }, diff --git a/src/AppConfig/appconfig.constant.ts b/src/AppConfig/appconfig.constant.ts index 008ef6d..d9bbcd0 100644 --- a/src/AppConfig/appconfig.constant.ts +++ b/src/AppConfig/appconfig.constant.ts @@ -22,25 +22,25 @@ export const ConfigConstants: Record< MID_DAY_BREAK_TIME_START: { name: 'Mid Day Break Time Start', key: 'MID_DAY_BREAK_TIME_START', - value: new Date(new Date().setHours(12, 0, 0, 0)).toISOString(), + value: new Date(new Date().setUTCHours(12, 0, 0, 0)).toISOString(), visible: true, }, MID_DAY_BREAK_TIME_END: { name: 'Mid Day Break Time End', key: 'MID_DAY_BREAK_TIME_END', - value: new Date(new Date().setHours(13, 0, 0, 0)).toISOString(), + value: new Date(new Date().setUTCHours(13, 0, 0, 0)).toISOString(), visible: true, }, SLOT_START_TIME: { name: 'Slot Start Time', key: 'SLOT_START_TIME', - value: new Date(new Date().setHours(8, 0, 0, 0)).toISOString(), + value: new Date(new Date().setUTCHours(8, 0, 0, 0)).toISOString(), visible: true, }, SLOT_END_TIME: { name: 'Slot End Time', key: 'SLOT_END_TIME', - value: new Date(new Date().setHours(22, 0, 0, 0)).toISOString(), + value: new Date(new Date().setUTCHours(22, 0, 0, 0)).toISOString(), visible: true, }, } diff --git a/src/AppConfig/appconfig.service.ts b/src/AppConfig/appconfig.service.ts index be95237..a06d4f6 100644 --- a/src/AppConfig/appconfig.service.ts +++ b/src/AppConfig/appconfig.service.ts @@ -32,6 +32,9 @@ export class AppConfigService implements OnModuleInit { where: { key }, }) } + async getConfigValue(key: string) { + return (await this.getConfig(key))?.value + } async getVisibleConfigs() { return await this.prisma.config.findMany({ diff --git a/src/Schedule/schedule.module.ts b/src/Schedule/schedule.module.ts index ba1a3e1..d02035a 100644 --- a/src/Schedule/schedule.module.ts +++ b/src/Schedule/schedule.module.ts @@ -1,8 +1,9 @@ import { Module } from '@nestjs/common' import { ScheduleSchema } from './schedule.schema' +import { ScheduleService } from './schedule.service' @Module({ - providers: [ScheduleSchema], - exports: [ScheduleSchema], + providers: [ScheduleSchema, ScheduleService], + exports: [ScheduleSchema, ScheduleService], }) export class ScheduleModule {} diff --git a/src/Schedule/schedule.schema.ts b/src/Schedule/schedule.schema.ts index 3278d5b..0d7d41a 100644 --- a/src/Schedule/schedule.schema.ts +++ b/src/Schedule/schedule.schema.ts @@ -8,12 +8,39 @@ import { import { Builder } from '../Graphql/graphql.builder' import { PrismaService } from '../Prisma/prisma.service' import { ScheduleStatus } from '@prisma/client' +import { ScheduleService } from './schedule.service' +import { AppConfigService } from '../AppConfig/appconfig.service' + +export type ScheduleConfigType = + | { + midDayBreakTimeStart?: string | null | undefined + midDayBreakTimeEnd?: string | null | undefined + slotDuration?: string | null | undefined + slotBreakDuration?: string | null | undefined + slotEndTime?: string | null | undefined + slotStartTime?: string | null | undefined + } + | null + | undefined + +export type ScheduleSlotType = { + slot: string + start: string + end: string +} + +export type PreviewScheduleType = { + totalSlots: number + slots: ScheduleSlotType[] +} @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() } @@ -53,6 +80,29 @@ export class ScheduleSchema extends PothosSchema { }) } + @PothosRef() + scheduleSlot() { + return this.builder.simpleObject('ScheduleSlot', { + fields: (t) => ({ + slot: t.string({}), + start: t.string({}), + end: t.string({}), + }), + }) + } + + @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', { @@ -79,6 +129,21 @@ export class ScheduleSchema extends PothosSchema { }) } + @PothosRef() + scheduleConfigInput() { + return this.builder.inputType('ScheduleConfigInput', { + description: 'A schedule config in the system.', + fields: (t) => ({ + midDayBreakTimeStart: t.string(), + midDayBreakTimeEnd: t.string(), + slotDuration: t.string(), + slotBreakDuration: t.string(), + slotEndTime: t.string(), + slotStartTime: t.string(), + }), + }) + } + @Pothos() init(): void { this.builder.queryFields((t) => ({ @@ -109,6 +174,21 @@ export class ScheduleSchema extends PothosSchema { }) }, }), + + 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) => { + return await this.scheduleService.createSchedulePreview( + args.scheduleConfig, + ) + }, + }), })) } } diff --git a/src/Schedule/schedule.service.ts b/src/Schedule/schedule.service.ts index bd84356..ade5ae6 100644 --- a/src/Schedule/schedule.service.ts +++ b/src/Schedule/schedule.service.ts @@ -1,10 +1,16 @@ import * as DateTimeUtils from '../common/utils/datetime.utils' -import { Injectable } from '@nestjs/common' +import { Injectable, Logger } from '@nestjs/common' import { PrismaService } from 'src/Prisma/prisma.service' -import { Schedule } from '@prisma/client' import { AppConfigService } from 'src/AppConfig/appconfig.service' +import { + PreviewScheduleType, + ScheduleConfigType, + ScheduleSlotType, +} from './schedule.schema' +import { Config } from '@prisma/client' +import * as _ from 'lodash' @Injectable() export class ScheduleService { @@ -13,10 +19,176 @@ export class ScheduleService { private readonly appConfigService: AppConfigService, ) {} - async createSchedule(schedule: Schedule) { - // get config - const config = await this.appConfigService.getVisibleConfigs() - console.log(config) + async createSchedulePreview( + scheduleConfig: ScheduleConfigType, + ): Promise { + const config: Config[] = await this.appConfigService.getVisibleConfigs() + Logger.log(config) + // process scheduleConfig input by filling with default values from config + const scheduleConfigFilled = this.processScheduleConfig( + scheduleConfig, + config, + ) // generate Slot By config + const slots = this.generateSlots(scheduleConfigFilled) + + return { + totalSlots: slots.length, + slots: slots, + } + } + + generateSlots(scheduleConfigFilled: ScheduleConfigType): ScheduleSlotType[] { + Logger.log(`Generating slots with config: ${scheduleConfigFilled}`) + const slots: ScheduleSlotType[] = [] + const numberOfSlots = this.calculateNumberOfSlots( + // @ts-ignore + scheduleConfigFilled?.slotStartTime, + // @ts-ignore + scheduleConfigFilled?.slotEndTime, + // @ts-ignore + scheduleConfigFilled?.slotDuration, + // @ts-ignore + scheduleConfigFilled?.slotBreakDuration, + ) + for (let i = 1; i <= numberOfSlots; i++) { + const { startTime, endTime } = this.getSlotStartAndEndTime( + i, + // @ts-ignore + scheduleConfigFilled?.slotDuration, + // @ts-ignore + scheduleConfigFilled?.slotBreakDuration, + // @ts-ignore + scheduleConfigFilled?.slotStartTime, + ) + // if the slot is not overlapping with mid day break time, add it to the slots + if ( + !this.isOverLapping( + startTime, + endTime, + this.getSpecificDateWithTime( + // @ts-ignore + scheduleConfigFilled?.midDayBreakTimeStart, + ), + this.getSpecificDateWithTime( + // @ts-ignore + scheduleConfigFilled?.midDayBreakTimeEnd, + ), + ) + ) { + slots.push({ + slot: i.toString(), + start: startTime.toISOString(), + end: endTime.toISOString(), + }) + } + } + return slots + } + + isOverLapping( + startTime1: Date, + endTime1: Date, + startTime2: Date, + endTime2: Date, + ) { + return ( + Math.max(startTime1.getTime(), startTime2.getTime()) < + Math.min(endTime1.getTime(), endTime2.getTime()) + ) + } + + calculateNumberOfSlots( + startTime: string, + endTime: string, + slotDuration: string, + slotBreakDuration: string, + ) { + const startDate = new Date(startTime) + const endDate = new Date(endTime) + + const totalMinutes = (endDate.getTime() - startDate.getTime()) / (60 * 1000) + const numberOfSlots = Math.floor( + totalMinutes / (parseInt(slotDuration) + parseInt(slotBreakDuration)), + ) + return numberOfSlots + } + + getSlotStartAndEndTime( + slotNumber: number, + slotDuration: string, + slotBreakDuration: string, + slotStartTime: string, + ) { + const startTime = new Date(slotStartTime) + startTime.setUTCMinutes( + startTime.getUTCMinutes() + + slotNumber * (parseInt(slotDuration) + parseInt(slotBreakDuration)), + ) + const endTime = new Date(startTime) + endTime.setUTCMinutes(endTime.getUTCMinutes() + parseInt(slotDuration)) + return { startTime, endTime } + } + + processScheduleConfig( + scheduleConfig: ScheduleConfigType, + config: Config[], + ): ScheduleConfigType { + // if scheduleConfig is undefined, create a new object and seed all the values with default values from config + if (scheduleConfig === undefined) { + scheduleConfig = config.reduce((acc, curr) => { + // biome-ignore lint/suspicious/noExplicitAny: + ;(acc as any)[this.sneakyCaseToCamelCase(curr.key)] = curr.value + return acc + }, {}) + } + // loop through scheduleConfig and fill with default values from config + for (const key in scheduleConfig) { + if ( + // biome-ignore lint/suspicious/noExplicitAny: + (scheduleConfig as any)[key] === undefined || + // biome-ignore lint/suspicious/noExplicitAny: + (scheduleConfig as any)[key] === null || + // biome-ignore lint/suspicious/noExplicitAny: + (scheduleConfig as any)[key] === '' + ) { + // biome-ignore lint/suspicious/noExplicitAny: + ;(scheduleConfig as any)[key] = + // biome-ignore lint/suspicious/noExplicitAny: + config[this.camelCaseToUpperSneakyCase(key) as any] + } + } + return scheduleConfig + } + + camelCaseToUpperSneakyCase(str: string) { + return _.snakeCase(str).toUpperCase() + } + + sneakyCaseToCamelCase(str: string) { + return _.camelCase(str.toLowerCase()) + } + + getTodayWithTime(date: Date) { + const today = new Date() + today.setUTCHours( + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), + 0, + ) + return today + } + + getSpecificDateWithTime(date: Date) { + const specificDate = new Date(date) + date = new Date(date) + specificDate.setUTCHours( + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), + 0, + ) + return specificDate } } diff --git a/src/Service/service.schema.ts b/src/Service/service.schema.ts index 8680050..ca2cd65 100644 --- a/src/Service/service.schema.ts +++ b/src/Service/service.schema.ts @@ -8,7 +8,7 @@ import { import { Builder } from '../Graphql/graphql.builder' import { PrismaService } from '../Prisma/prisma.service' import { MinioService } from '../Minio/minio.service' -import { ServiceStatus } from '@prisma/client' +import { Role, ServiceStatus } from '@prisma/client' import { MailService } from '../Mail/mail.service' @Injectable() export class ServiceSchema extends PothosSchema { @@ -119,7 +119,7 @@ export class ServiceSchema extends PothosSchema { type: this.service(), cursor: 'id', args: this.builder.generator.findManyArgs('Service'), - resolve: async (query, root, args, ctx, info) => { + resolve: async (query, _root, _args, _ctx, _info) => { return await this.prisma.service.findMany({ ...query, }) @@ -136,9 +136,57 @@ export class ServiceSchema extends PothosSchema { description: 'Retrieve a list of services with optional filtering, ordering, and pagination.', type: [this.service()], + args: this.builder.generator.findManyArgs('Service'), + resolve: async (query, _root, args, _ctx, _info) => { + return await this.prisma.service.findMany({ + ...query, + where: args.filter ?? undefined, + orderBy: args.orderBy ?? undefined, + skip: args.skip ?? undefined, + take: args.take ?? undefined, + cursor: args.cursor ?? undefined, + }) + }, + }), + servicesByCenter: t.prismaField({ + description: + 'Retrieve a list of services with optional filtering, ordering, and pagination.', + type: [this.service()], args: this.builder.generator.findManyArgs('Service'), - resolve: async (query, root, args, ctx, info) => { + resolve: async (query, _root, args, ctx, _info) => { + if (ctx.isSubscription) { + throw new Error('Not allowed') + } + // check role if user is mentor or center owner + const role = ctx.http.me.role + if (role !== Role.CENTER_MENTOR && role !== Role.CENTER_OWNER) { + throw new Error('Not allowed') + } + if (role === Role.CENTER_MENTOR) { + // load only service belong to center of current user + const managedServices = await this.prisma.managedService.findMany({ + where: { mentorId: ctx.http.me.id }, + }) + if (!managedServices) { + throw new Error('Managed services not found') + } + // query services that have serviceId in managedServices + args.filter = { + id: { in: managedServices.map((service) => service.serviceId) }, + } + } + // if role is center owner, load all services belong to center of current user + if (role === Role.CENTER_OWNER) { + const center = await this.prisma.center.findUnique({ + where: { centerOwnerId: ctx.http.me.id }, + }) + if (!center) { + throw new Error('Center not found') + } + args.filter = { centerId: { in: [center.id] } } + } + return await this.prisma.service.findMany({ ...query, where: args.filter ?? undefined, @@ -158,7 +206,7 @@ export class ServiceSchema extends PothosSchema { required: true, }), }, - resolve: async (query, root, args, ctx, info) => { + resolve: async (query, _root, args, _ctx, _info) => { return await this.prisma.service.findUnique({ ...query, where: args.input, @@ -177,11 +225,16 @@ export class ServiceSchema extends PothosSchema { type: this.service(), args: { input: t.arg({ - type: this.builder.generator.getCreateInput('Service'), + type: this.builder.generator.getCreateInput('Service', ['user']), required: true, }), }, - resolve: async (query, root, args, ctx, info) => { + resolve: async (query, _root, args, ctx, _info) => { + if (ctx.isSubscription) { + throw new Error('Not allowed') + } + // replace userId with current user id + args.input.user = { connect: { id: ctx.http.me.id } } return await this.prisma.service.create({ ...query, data: args.input, @@ -201,7 +254,7 @@ export class ServiceSchema extends PothosSchema { required: true, }), }, - resolve: async (query, root, args, ctx, info) => { + resolve: async (query, _root, args, _ctx, _info) => { return await this.prisma.service.update({ ...query, where: args.where, @@ -218,7 +271,7 @@ export class ServiceSchema extends PothosSchema { required: true, }), }, - resolve: async (query, root, args, ctx, info) => { + resolve: async (query, _root, args, _ctx, _info) => { return await this.prisma.service.delete({ ...query, where: args.where, @@ -242,7 +295,7 @@ export class ServiceSchema extends PothosSchema { required: false, }), }, - resolve: async (query, root, args, ctx, info) => { + resolve: async (query, _root, args, ctx, _info) => { if (ctx.isSubscription) { throw new Error('Not allowed') }