fix some bug and produce many problem to solve later
This commit is contained in:
@@ -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().setUTCHours(12, 0, 0, 0)).toISOString(),
|
||||
value: '12:00:00',
|
||||
visible: true,
|
||||
},
|
||||
MID_DAY_BREAK_TIME_END: {
|
||||
name: 'Mid Day Break Time End',
|
||||
key: 'MID_DAY_BREAK_TIME_END',
|
||||
value: new Date(new Date().setUTCHours(13, 0, 0, 0)).toISOString(),
|
||||
value: '13:00:00',
|
||||
visible: true,
|
||||
},
|
||||
SLOT_START_TIME: {
|
||||
name: 'Slot Start Time',
|
||||
key: 'SLOT_START_TIME',
|
||||
value: new Date(new Date().setUTCHours(8, 0, 0, 0)).toISOString(),
|
||||
DAY_START_TIME: {
|
||||
name: 'Day Start Time',
|
||||
key: 'DAY_START_TIME',
|
||||
value: '08:00:00',
|
||||
visible: true,
|
||||
},
|
||||
SLOT_END_TIME: {
|
||||
name: 'Slot End Time',
|
||||
key: 'SLOT_END_TIME',
|
||||
value: new Date(new Date().setUTCHours(22, 0, 0, 0)).toISOString(),
|
||||
DAY_END_TIME: {
|
||||
name: 'Day End Time',
|
||||
key: 'DAY_END_TIME',
|
||||
value: '22:00:00',
|
||||
visible: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Injectable, OnModuleInit } from '@nestjs/common'
|
||||
import { PrismaService } from 'src/Prisma/prisma.service'
|
||||
import { ConfigConstants } from './appconfig.constant'
|
||||
import { Config } from '@prisma/client'
|
||||
import { StringUtils } from 'src/common/utils/string.utils'
|
||||
|
||||
@Injectable()
|
||||
export class AppConfigService implements OnModuleInit {
|
||||
@@ -37,9 +38,14 @@ export class AppConfigService implements OnModuleInit {
|
||||
}
|
||||
|
||||
async getVisibleConfigs() {
|
||||
return await this.prisma.config.findMany({
|
||||
const configs = await this.prisma.config.findMany({
|
||||
where: { visible: true },
|
||||
})
|
||||
// map to camelCase
|
||||
return configs.map((config) => ({
|
||||
...config,
|
||||
key: StringUtils.sneakyCaseToCamelCase(config.key),
|
||||
}))
|
||||
}
|
||||
|
||||
async updateConfig(key: string, value: string) {
|
||||
@@ -63,6 +69,16 @@ export class AppConfigService implements OnModuleInit {
|
||||
})
|
||||
}
|
||||
|
||||
async getDefaultConfig() {
|
||||
return Object.entries(ConfigConstants).reduce((acc, [key, value]) => {
|
||||
if (value.visible) {
|
||||
// @ts-ignore
|
||||
acc[key] = value.value
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
async resetAllConfigs() {
|
||||
// reset all configs to default values
|
||||
Object.entries(ConfigConstants).forEach(async ([_key, value]) => {
|
||||
|
||||
@@ -22,6 +22,9 @@ import SchemaBuilder from '@pothos/core'
|
||||
import SimpleObjectPlugin from '@pothos/plugin-simple-objects'
|
||||
import { User } from '@prisma/client'
|
||||
import { getDatamodel } from '../types/pothos.generated'
|
||||
import { DateTime } from 'luxon'
|
||||
import { Kind } from 'graphql'
|
||||
import { DateTimeUtils } from '../common/utils/datetime.utils'
|
||||
|
||||
// import { rules } from '../common/graphql/common.graphql.auth-rule';
|
||||
|
||||
@@ -29,6 +32,7 @@ export type SchemaContext =
|
||||
| {
|
||||
isSubscription: true
|
||||
websocket: {
|
||||
req: Request
|
||||
pubSub: PubSub
|
||||
me: User
|
||||
generator: PrismaCrudGenerator<BuilderTypes>
|
||||
@@ -57,8 +61,8 @@ export interface SchemaBuilderOption {
|
||||
// AuthZRule: keyof typeof rules;
|
||||
Scalars: {
|
||||
DateTime: {
|
||||
Input: Date
|
||||
Output: Date
|
||||
Input: string | DateTime | Date
|
||||
Output: string | DateTime | Date
|
||||
}
|
||||
Json: {
|
||||
Input: JSON
|
||||
@@ -112,7 +116,36 @@ export class Builder extends SchemaBuilder<SchemaBuilderOption> {
|
||||
},
|
||||
})
|
||||
this.generator = new PrismaCrudGenerator<BuilderTypes>(this)
|
||||
this.addScalarType('DateTime', DateTimeResolver)
|
||||
this.scalarType('DateTime', {
|
||||
serialize: (value) => {
|
||||
// Serialize outgoing DateTime to ISO string
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'object' && value !== null && 'toISO' in value) {
|
||||
return value
|
||||
}
|
||||
// if value = Date, convert to DateTime
|
||||
if (value instanceof Date) {
|
||||
return DateTimeUtils.toIsoString(DateTimeUtils.fromDate(value))
|
||||
}
|
||||
throw new Error('Invalid DateTime')
|
||||
},
|
||||
parseValue: (value) => {
|
||||
// Parse incoming ISO string to Luxon DateTime
|
||||
if (typeof value === 'string') {
|
||||
return DateTimeUtils.fromIsoString(value)
|
||||
}
|
||||
throw new Error('Invalid DateTime')
|
||||
},
|
||||
parseLiteral: (ast) => {
|
||||
// parse string to DateTime
|
||||
if (ast.kind === Kind.STRING) {
|
||||
return DateTimeUtils.fromIsoString(ast.value)
|
||||
}
|
||||
throw new Error('Invalid DateTime')
|
||||
},
|
||||
})
|
||||
this.addScalarType('Json', JSONObjectResolver)
|
||||
this.addScalarType('Upload', GraphQLUpload)
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ import { WorkshopModule } from '../Workshop/workshop.module'
|
||||
import { WorkshopOrganizationModule } from '../WorkshopOrganization/workshoporganization.module'
|
||||
import { WorkshopSubscriptionModule } from '../WorkshopSubscription/workshopsubscription.module'
|
||||
import { initContextCache } from '@pothos/core'
|
||||
import { PubSub } from 'graphql-subscriptions'
|
||||
import { isSubscription } from 'rxjs/internal/Subscription'
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
@@ -81,8 +83,8 @@ import { initContextCache } from '@pothos/core'
|
||||
}),
|
||||
GraphQLModule.forRootAsync<ApolloDriverConfig>({
|
||||
driver: PothosApolloDriver,
|
||||
inject: [GraphqlService],
|
||||
useFactory: async (graphqlService: GraphqlService) => ({
|
||||
inject: [GraphqlService, 'PUB_SUB'],
|
||||
useFactory: async (graphqlService: GraphqlService, pubsub: PubSub) => ({
|
||||
path: process.env.API_PATH + '/graphql',
|
||||
debug: process.env.NODE_ENV === 'development' || false,
|
||||
playground: process.env.NODE_ENV === 'development' || false,
|
||||
@@ -91,18 +93,36 @@ import { initContextCache } from '@pothos/core'
|
||||
subscriptions: {
|
||||
'graphql-ws': true,
|
||||
},
|
||||
context: async ({ req }: { req: Request }) => ({
|
||||
...initContextCache(),
|
||||
isSubscription: false,
|
||||
http: {
|
||||
req,
|
||||
me: await graphqlService.acquireContext(req),
|
||||
invalidateCache: () =>
|
||||
graphqlService.invalidateCache(
|
||||
req.headers['x-session-id'] as string,
|
||||
),
|
||||
},
|
||||
}),
|
||||
context: async ({
|
||||
req,
|
||||
subscriptions,
|
||||
extra,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
}: { req?: Request; subscriptions?: any; extra?: any }) => {
|
||||
if (subscriptions) {
|
||||
return {
|
||||
isSubscription: true,
|
||||
websocket: {
|
||||
req: extra.request,
|
||||
pubSub: pubsub,
|
||||
me: await graphqlService.acquireContext(
|
||||
extra.request.headers['x-session-id'],
|
||||
),
|
||||
generator: extra.schemaBuilder,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
isSubscription: false,
|
||||
http: {
|
||||
req,
|
||||
me: req ? await graphqlService.acquireContext(req) : null,
|
||||
pubSub: pubsub,
|
||||
invalidateCache: () => Promise.resolve(),
|
||||
generator: extra.schemaBuilder,
|
||||
},
|
||||
}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
@@ -124,7 +144,17 @@ import { initContextCache } from '@pothos/core'
|
||||
useFactory: (builder: Builder) => new PrismaCrudGenerator(builder),
|
||||
inject: [Builder],
|
||||
},
|
||||
{
|
||||
provide: 'PUB_SUB',
|
||||
useFactory: () => new PubSub(),
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
Builder,
|
||||
PrismaCrudGenerator,
|
||||
GraphqlService,
|
||||
RedisService,
|
||||
'PUB_SUB',
|
||||
],
|
||||
exports: [Builder, PrismaCrudGenerator, GraphqlService, RedisService],
|
||||
})
|
||||
export class GraphqlModule {}
|
||||
|
||||
@@ -24,12 +24,8 @@ export class GraphqlService {
|
||||
const disableAuth = process.env.DISABLE_AUTH === 'true'
|
||||
try {
|
||||
sessionId = req.headers['x-session-id'] as string
|
||||
} catch (error) {
|
||||
Logger.error('Error acquiring context', error)
|
||||
if (disableAuth) {
|
||||
return null
|
||||
}
|
||||
throw new UnauthorizedException('Must provide a session ID')
|
||||
} catch (_error) {
|
||||
return null
|
||||
}
|
||||
if (disableAuth) {
|
||||
return null
|
||||
|
||||
@@ -118,22 +118,17 @@ export class MessageSchema extends PothosSchema {
|
||||
}),
|
||||
}))
|
||||
|
||||
// subscriptions
|
||||
/* The code snippet `subscriptions` is currently commented out in the provided TypeScript class. It
|
||||
appears to be a placeholder or a section where subscription-related logic or fields could be
|
||||
defined. In GraphQL, subscriptions are used to listen for real-time events or changes in data
|
||||
and receive updates when those events occur. */
|
||||
|
||||
this.builder.subscriptionFields((t) => ({
|
||||
messageSent: t.field({
|
||||
subscribe: (_, __, ctx) => {
|
||||
description: 'Subscribe to messages sent by users.',
|
||||
args: {},
|
||||
subscribe: async (_, __, ctx) => {
|
||||
if (!ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
}
|
||||
return {
|
||||
[Symbol.asyncIterator]: () =>
|
||||
ctx.websocket.pubSub.asyncIterator('MESSAGE_SENT'),
|
||||
}
|
||||
return (await ctx.websocket.pubSub.asyncIterator(
|
||||
'MESSAGE_SENT',
|
||||
)) as unknown as AsyncIterable<unknown>
|
||||
},
|
||||
type: this.message(), // Add the type property
|
||||
resolve: (payload) =>
|
||||
|
||||
8
src/Realtime/realtime.module.ts
Normal file
8
src/Realtime/realtime.module.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { RealtimeService } from './realtime.service'
|
||||
|
||||
@Module({
|
||||
providers: [RealtimeService],
|
||||
exports: [RealtimeService],
|
||||
})
|
||||
export class RealtimeModule {}
|
||||
7
src/Realtime/realtime.service.ts
Normal file
7
src/Realtime/realtime.service.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
// @ts-ignore
|
||||
import * as Y from 'yjs'
|
||||
@Injectable()
|
||||
export class RealtimeService {
|
||||
yjs = new Y.Doc()
|
||||
}
|
||||
65
src/Schedule/schedule.d.ts
vendored
Normal file
65
src/Schedule/schedule.d.ts
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
export interface ScheduleDateInput {
|
||||
scheduleId: string
|
||||
start: string
|
||||
end: string
|
||||
dayOfWeek: number
|
||||
slot: number
|
||||
serviceId: string
|
||||
orderId: string | null
|
||||
}
|
||||
|
||||
export interface ScheduleConfigType {
|
||||
midDayBreakTimeStart: string
|
||||
midDayBreakTimeEnd: string
|
||||
slotDuration: string
|
||||
slotBreakDuration: string
|
||||
dayStartTime: string
|
||||
dayEndTime: string
|
||||
}
|
||||
|
||||
export interface ScheduleConfigTypeForCenter {
|
||||
startDate: string
|
||||
endDate: string
|
||||
slots: number[]
|
||||
days: number[]
|
||||
}
|
||||
|
||||
export interface ScheduleSlotType {
|
||||
slot: string
|
||||
dayOfWeek: number
|
||||
start: string
|
||||
end: string
|
||||
}
|
||||
|
||||
export interface PreviewScheduleType {
|
||||
totalSlots: number
|
||||
slots: ScheduleSlotType[]
|
||||
}
|
||||
|
||||
export interface ScheduleConfigType {
|
||||
midDayBreakTimeStart: string
|
||||
midDayBreakTimeEnd: string
|
||||
slotDuration: string
|
||||
slotBreakDuration: string
|
||||
dayStartTime: string
|
||||
dayEndTime: string
|
||||
}
|
||||
|
||||
export interface ScheduleConfigTypeForCenter {
|
||||
startDate: string
|
||||
endDate: string
|
||||
slots: number[]
|
||||
days: number[]
|
||||
}
|
||||
|
||||
export interface ScheduleSlotType {
|
||||
slot: string
|
||||
dayOfWeek: number
|
||||
start: string
|
||||
end: string
|
||||
}
|
||||
|
||||
export interface PreviewScheduleType {
|
||||
totalSlots: number
|
||||
slots: ScheduleSlotType[]
|
||||
}
|
||||
@@ -10,37 +10,7 @@ import { PrismaService } from '../Prisma/prisma.service'
|
||||
import { ScheduleStatus } from '@prisma/client'
|
||||
import { ScheduleService } from './schedule.service'
|
||||
import { AppConfigService } from '../AppConfig/appconfig.service'
|
||||
import { forEach } from 'lodash'
|
||||
|
||||
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 ScheduleConfigTypeForCenter = {
|
||||
startDate: string
|
||||
endDate: string
|
||||
slots: number[]
|
||||
days: number[]
|
||||
}
|
||||
|
||||
export type ScheduleSlotType = {
|
||||
slot: string
|
||||
start: string
|
||||
end: string
|
||||
}
|
||||
|
||||
export type PreviewScheduleType = {
|
||||
totalSlots: number
|
||||
slots: ScheduleSlotType[]
|
||||
}
|
||||
import { ScheduleConfigType } from './schedule'
|
||||
|
||||
@Injectable()
|
||||
export class ScheduleSchema extends PothosSchema {
|
||||
@@ -109,6 +79,7 @@ export class ScheduleSchema extends PothosSchema {
|
||||
slot: t.string({}),
|
||||
start: t.string({}),
|
||||
end: t.string({}),
|
||||
dayOfWeek: t.int({}),
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -168,12 +139,24 @@ export class ScheduleSchema extends PothosSchema {
|
||||
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(),
|
||||
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,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -254,7 +237,17 @@ export class ScheduleSchema extends PothosSchema {
|
||||
}),
|
||||
},
|
||||
resolve: async (_parent, args, _context, _info) => {
|
||||
return await this.scheduleService.createSchedulePreview(
|
||||
// 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,
|
||||
)
|
||||
},
|
||||
@@ -278,6 +271,7 @@ d72a864e-2f41-45ab-9c9b-bf0512a31883,e9be51fd-2382-4e43-9988-74e76fde4b56,2024-1
|
||||
'status',
|
||||
'customerId',
|
||||
'orderId',
|
||||
'dates',
|
||||
]),
|
||||
required: true,
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as DateTimeUtils from '../common/utils/datetime.utils'
|
||||
import { DateTimeUtils } from '../common/utils/datetime.utils'
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common'
|
||||
import { PrismaService } from 'src/Prisma/prisma.service'
|
||||
@@ -9,28 +9,12 @@ import {
|
||||
ScheduleConfigType,
|
||||
ScheduleConfigTypeForCenter,
|
||||
ScheduleSlotType,
|
||||
} from './schedule.schema'
|
||||
} from './schedule.d'
|
||||
import { Config, Schedule, ScheduleDate } from '@prisma/client'
|
||||
import { DateTime, Settings, Zone } from 'luxon'
|
||||
import * as _ from 'lodash'
|
||||
import { ScheduleDateInput } from './schedule'
|
||||
|
||||
Settings.defaultLocale = 'en-US'
|
||||
Settings.defaultZone = 'utc'
|
||||
Settings.defaultWeekSettings = {
|
||||
firstDay: 2,
|
||||
minimalDays: 1,
|
||||
weekend: [6, 7],
|
||||
}
|
||||
|
||||
interface ScheduleDateInput {
|
||||
scheduleId: string
|
||||
start: string
|
||||
end: string
|
||||
dayOfWeek: number
|
||||
slot: number
|
||||
serviceId: string
|
||||
orderId: string | null
|
||||
}
|
||||
@Injectable()
|
||||
export class ScheduleService {
|
||||
constructor(
|
||||
@@ -38,17 +22,11 @@ export class ScheduleService {
|
||||
private readonly appConfigService: AppConfigService,
|
||||
) {}
|
||||
|
||||
async createSchedulePreview(
|
||||
async createSchedulePreviewForSingleDay(
|
||||
scheduleConfig: ScheduleConfigType,
|
||||
): Promise<PreviewScheduleType> {
|
||||
const config: Config[] = await this.appConfigService.getVisibleConfigs()
|
||||
// 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)
|
||||
const slots = this.generateSlots(scheduleConfig)
|
||||
|
||||
return {
|
||||
totalSlots: slots.length,
|
||||
@@ -60,7 +38,13 @@ export class ScheduleService {
|
||||
async createSchedulePreviewForCenter(
|
||||
scheduleConfig: ScheduleConfigTypeForCenter,
|
||||
): Promise<PreviewScheduleType> {
|
||||
const config: Config[] = await this.appConfigService.getVisibleConfigs()
|
||||
const config: ScheduleConfigType = (
|
||||
await this.appConfigService.getVisibleConfigs()
|
||||
).reduce((acc, curr) => {
|
||||
// @ts-ignore
|
||||
acc[curr.key] = curr.value
|
||||
return acc
|
||||
}, {} as ScheduleConfigType)
|
||||
const slots = this.generateSlotsPreviewForCenter(scheduleConfig, config)
|
||||
return {
|
||||
totalSlots: slots.length,
|
||||
@@ -70,30 +54,41 @@ export class ScheduleService {
|
||||
|
||||
async generateScheduleDates(schedule: Schedule): Promise<ScheduleDate[]> {
|
||||
// generate schedule dates based on data and config
|
||||
const config: Config[] = await this.appConfigService.getVisibleConfigs()
|
||||
const config: ScheduleConfigType = (
|
||||
await this.appConfigService.getVisibleConfigs()
|
||||
).reduce((acc, curr) => {
|
||||
// @ts-ignore
|
||||
acc[curr.key] = curr.value
|
||||
return acc
|
||||
}, {} as ScheduleConfigType)
|
||||
|
||||
const daysOfWeeks = schedule.daysOfWeek
|
||||
const slots = schedule.slots
|
||||
const scheduleStart = schedule.scheduleStart
|
||||
const scheduleEnd = schedule.scheduleEnd
|
||||
const slotDuration = config.find((c) => c.key === 'SLOT_DURATION')?.value
|
||||
const slotBreakDuration = config.find(
|
||||
(c) => c.key === 'SLOT_BREAK_DURATION',
|
||||
)?.value
|
||||
const slotStartTime = config.find((c) => c.key === 'SLOT_START_TIME')?.value
|
||||
const scheduleStart = DateTime.fromJSDate(schedule.scheduleStart)
|
||||
const scheduleEnd = DateTime.fromJSDate(schedule.scheduleEnd)
|
||||
const slotDuration = parseInt(config.slotDuration)
|
||||
const slotBreakDuration = parseInt(config.slotBreakDuration)
|
||||
|
||||
const scheduleDates: ScheduleDateInput[] = []
|
||||
|
||||
// loop each day from scheduleStart to scheduleEnd
|
||||
let date = DateTime.fromJSDate(scheduleStart)
|
||||
while (date <= DateTime.fromJSDate(scheduleEnd)) {
|
||||
for (
|
||||
let date = scheduleStart;
|
||||
date <= scheduleEnd;
|
||||
date = date.plus({ days: 1 })
|
||||
) {
|
||||
// Check if the current date matches one of the specified days of the week
|
||||
if (daysOfWeeks.includes(date.weekday)) {
|
||||
// loop through slots
|
||||
for (const slot of slots) {
|
||||
const { startTime, endTime } = this.getSlotStartAndEndTime(
|
||||
slot,
|
||||
slotDuration ?? '',
|
||||
slotBreakDuration ?? '',
|
||||
slotStartTime ?? '',
|
||||
slotDuration.toString(),
|
||||
slotBreakDuration.toString(),
|
||||
DateTimeUtils.getATimeWithDateB(
|
||||
DateTime.fromISO(config.dayStartTime),
|
||||
date,
|
||||
).toISO() ?? '',
|
||||
)
|
||||
scheduleDates.push({
|
||||
scheduleId: schedule.id,
|
||||
@@ -106,9 +101,8 @@ export class ScheduleService {
|
||||
})
|
||||
}
|
||||
}
|
||||
// Move to the next day
|
||||
date = date.plus({ days: 1 })
|
||||
}
|
||||
|
||||
const scheduleDatesCreated =
|
||||
await this.prisma.scheduleDate.createManyAndReturn({
|
||||
data: scheduleDates,
|
||||
@@ -117,100 +111,102 @@ export class ScheduleService {
|
||||
return scheduleDatesCreated
|
||||
}
|
||||
|
||||
/*
|
||||
example query:
|
||||
query CenterPreviewSchedule {
|
||||
centerPreviewSchedule(
|
||||
scheduleConfigInput: { days: [3,5], endDate: "2024-11-22T00:00:00.000Z", slots: [2,6], startDate: "2024-11-02T00:00:00.000Z" }
|
||||
) {
|
||||
totalSlots
|
||||
slots {
|
||||
dayOfWeek
|
||||
end
|
||||
slot
|
||||
start
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
generateSlotsPreviewForCenter(
|
||||
scheduleConfig: ScheduleConfigTypeForCenter,
|
||||
config: Config[],
|
||||
_scheduleConfig: ScheduleConfigTypeForCenter,
|
||||
_config: ScheduleConfigType,
|
||||
): ScheduleSlotType[] {
|
||||
const startDate = DateTime.fromISO(scheduleConfig.startDate)
|
||||
const endDate = DateTime.fromISO(scheduleConfig.endDate)
|
||||
const daysOfWeeks = scheduleConfig.days
|
||||
|
||||
// Retrieve slot configuration values once
|
||||
const slotDuration =
|
||||
config.find((c) => c.key === 'SLOT_DURATION')?.value ?? ''
|
||||
const slotBreakDuration =
|
||||
config.find((c) => c.key === 'SLOT_BREAK_DURATION')?.value ?? ''
|
||||
const slotStartTime =
|
||||
config.find((c) => c.key === 'SLOT_START_TIME')?.value ?? ''
|
||||
const slotEndTime =
|
||||
config.find((c) => c.key === 'SLOT_END_TIME')?.value ?? ''
|
||||
|
||||
// Calculate the number of slots based on configuration
|
||||
const numberOfSlots = this.calculateNumberOfSlots(
|
||||
slotStartTime,
|
||||
slotEndTime,
|
||||
slotDuration,
|
||||
slotBreakDuration,
|
||||
)
|
||||
|
||||
const slots: ScheduleSlotType[] = []
|
||||
|
||||
// Loop through each day between start and end dates
|
||||
for (let date = startDate; date <= endDate; date = date.plus({ days: 1 })) {
|
||||
const daysOfWeeks = _scheduleConfig.days
|
||||
const scheduleStart = DateTime.fromISO(_scheduleConfig.startDate)
|
||||
const scheduleEnd = DateTime.fromISO(_scheduleConfig.endDate)
|
||||
// loop each day from scheduleStart to scheduleEnd
|
||||
for (
|
||||
let date = scheduleStart;
|
||||
date <= scheduleEnd;
|
||||
date = date.plus({ days: 1 })
|
||||
) {
|
||||
// Check if the current date matches one of the specified days of the week
|
||||
if (daysOfWeeks.includes(date.weekday)) {
|
||||
Logger.log(`Generating slots for date: ${date.toISO()}`)
|
||||
// For each slot number, calculate start and end times
|
||||
for (let i = 1; i <= numberOfSlots; i++) {
|
||||
// loop through slots
|
||||
for (const slot of _scheduleConfig.slots) {
|
||||
// get slot start and end time
|
||||
const { startTime, endTime } = this.getSlotStartAndEndTime(
|
||||
i,
|
||||
slotDuration,
|
||||
slotBreakDuration,
|
||||
slotStartTime,
|
||||
slot,
|
||||
_config.slotDuration,
|
||||
_config.slotBreakDuration,
|
||||
DateTimeUtils.getATimeWithDateB(
|
||||
DateTime.fromISO(_config.dayStartTime),
|
||||
date,
|
||||
).toISO() ?? '',
|
||||
)
|
||||
|
||||
slots.push({
|
||||
slot: i.toString(),
|
||||
start: startTime.toISO() ?? '',
|
||||
end: endTime.toISO() ?? '',
|
||||
})
|
||||
// if the slot is not overlapping with mid day break time, add it to the slots
|
||||
if (
|
||||
!DateTimeUtils.isOverlap(
|
||||
startTime,
|
||||
endTime,
|
||||
DateTimeUtils.fromIsoString(_config.midDayBreakTimeStart),
|
||||
DateTimeUtils.fromIsoString(_config.midDayBreakTimeEnd),
|
||||
)
|
||||
) {
|
||||
slots.push({
|
||||
slot: slot.toString(),
|
||||
start: startTime.toString(),
|
||||
end: endTime.toString(),
|
||||
dayOfWeek: date.weekday,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return slots
|
||||
}
|
||||
|
||||
generateSlots(scheduleConfigFilled: ScheduleConfigType): ScheduleSlotType[] {
|
||||
generateSlots(scheduleConfig: ScheduleConfigType): ScheduleSlotType[] {
|
||||
const slots: ScheduleSlotType[] = []
|
||||
const numberOfSlots = this.calculateNumberOfSlots(
|
||||
// @ts-ignore
|
||||
scheduleConfigFilled?.slotStartTime,
|
||||
// @ts-ignore
|
||||
scheduleConfigFilled?.slotEndTime,
|
||||
// @ts-ignore
|
||||
scheduleConfigFilled?.slotDuration,
|
||||
// @ts-ignore
|
||||
scheduleConfigFilled?.slotBreakDuration,
|
||||
scheduleConfig.dayStartTime,
|
||||
scheduleConfig.dayEndTime,
|
||||
scheduleConfig.slotDuration,
|
||||
scheduleConfig.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,
|
||||
scheduleConfig.slotDuration,
|
||||
scheduleConfig.slotBreakDuration,
|
||||
scheduleConfig.dayStartTime,
|
||||
)
|
||||
// if the slot is not overlapping with mid day break time, add it to the slots
|
||||
if (
|
||||
!this.isOverLapping(
|
||||
!DateTimeUtils.isOverlap(
|
||||
startTime,
|
||||
endTime,
|
||||
DateTime.fromISO(
|
||||
// @ts-ignore
|
||||
scheduleConfigFilled?.midDayBreakTimeStart,
|
||||
),
|
||||
DateTime.fromISO(
|
||||
// @ts-ignore
|
||||
scheduleConfigFilled?.midDayBreakTimeEnd,
|
||||
),
|
||||
DateTimeUtils.fromIsoString(scheduleConfig.midDayBreakTimeStart),
|
||||
DateTimeUtils.fromIsoString(scheduleConfig.midDayBreakTimeEnd),
|
||||
)
|
||||
) {
|
||||
slots.push({
|
||||
slot: i.toString(),
|
||||
start: startTime.toString(),
|
||||
end: endTime.toString(),
|
||||
dayOfWeek: startTime.weekday,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -235,13 +231,26 @@ export class ScheduleService {
|
||||
slotDuration: string,
|
||||
slotBreakDuration: string,
|
||||
) {
|
||||
const startDate = DateTime.fromISO(startTime)
|
||||
const endDate = DateTime.fromISO(endTime)
|
||||
const _startTime = DateTimeUtils.toTime(startTime)
|
||||
const _endTime = DateTimeUtils.toTime(endTime)
|
||||
const _slotDuration = parseInt(slotDuration) // minutes
|
||||
const _slotBreakDuration = parseInt(slotBreakDuration) // minutes
|
||||
|
||||
const startDate = DateTime.fromObject({
|
||||
hour: _startTime.hour,
|
||||
minute: _startTime.minute,
|
||||
second: _startTime.second,
|
||||
})
|
||||
const endDate = DateTime.fromObject({
|
||||
hour: _endTime.hour,
|
||||
minute: _endTime.minute,
|
||||
second: _endTime.second,
|
||||
})
|
||||
|
||||
const totalMinutes =
|
||||
(endDate.toMillis() - startDate.toMillis()) / (60 * 1000)
|
||||
const numberOfSlots = Math.floor(
|
||||
totalMinutes / (parseInt(slotDuration) + parseInt(slotBreakDuration)),
|
||||
totalMinutes / (_slotDuration + _slotBreakDuration),
|
||||
)
|
||||
return numberOfSlots
|
||||
}
|
||||
@@ -250,12 +259,13 @@ export class ScheduleService {
|
||||
slotNumber: number,
|
||||
slotDuration: string,
|
||||
slotBreakDuration: string,
|
||||
slotStartTime: string,
|
||||
dayStartTime: string,
|
||||
) {
|
||||
const durationInMinutes = parseInt(slotDuration)
|
||||
const breakDurationInMinutes = parseInt(slotBreakDuration)
|
||||
const initialStartTime = DateTime.fromISO(dayStartTime)
|
||||
|
||||
const startTime = DateTime.fromISO(slotStartTime).plus({
|
||||
const startTime = initialStartTime.plus({
|
||||
minutes: (slotNumber - 1) * (durationInMinutes + breakDurationInMinutes),
|
||||
})
|
||||
|
||||
@@ -265,63 +275,4 @@ export class ScheduleService {
|
||||
endTime: 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: <explanation>
|
||||
;(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: <explanation>
|
||||
(scheduleConfig as any)[key] === undefined ||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
(scheduleConfig as any)[key] === null ||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
(scheduleConfig as any)[key] === ''
|
||||
) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
;(scheduleConfig as any)[key] =
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
config[this.camelCaseToUpperSneakyCase(key) as any]
|
||||
}
|
||||
}
|
||||
return scheduleConfig
|
||||
}
|
||||
|
||||
camelCaseToUpperSneakyCase(str: string) {
|
||||
return _.snakeCase(str).toUpperCase()
|
||||
}
|
||||
|
||||
sneakyCaseToCamelCase(str: string) {
|
||||
return _.camelCase(str.toLowerCase())
|
||||
}
|
||||
|
||||
getTodayWithTime(date: DateTime) {
|
||||
let today = DateTime.now()
|
||||
today = today.set({
|
||||
hour: date.hour,
|
||||
minute: date.minute,
|
||||
second: date.second,
|
||||
})
|
||||
return today
|
||||
}
|
||||
|
||||
getSpecificDateWithTime(date: DateTime) {
|
||||
let specificDate = DateTime.now()
|
||||
specificDate = specificDate.set({
|
||||
hour: date.hour,
|
||||
minute: date.minute,
|
||||
second: date.second,
|
||||
})
|
||||
return specificDate
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,115 @@
|
||||
// function getOverlapRange(
|
||||
// startA: number,
|
||||
// endA: number,
|
||||
// startB: number,
|
||||
// endB: number,
|
||||
// ) {
|
||||
// const overlapStart = Math.max(startA, startB);
|
||||
// const overlapEnd = Math.min(endA, endB);
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import * as _ from 'lodash'
|
||||
import {
|
||||
DateTime,
|
||||
Settings,
|
||||
HourNumbers,
|
||||
MinuteNumbers,
|
||||
SecondNumbers,
|
||||
DayNumbers,
|
||||
WeekdayNumbers,
|
||||
} from 'luxon'
|
||||
|
||||
// return overlapStart < overlapEnd ? { overlapStart, overlapEnd } : null;
|
||||
// }
|
||||
Settings.defaultLocale = 'en-US'
|
||||
Settings.defaultZone = 'utc'
|
||||
Settings.defaultWeekSettings = {
|
||||
firstDay: 2,
|
||||
minimalDays: 1,
|
||||
weekend: [6, 7],
|
||||
}
|
||||
|
||||
// export function isOverlap(
|
||||
// startA: number,
|
||||
// endA: number,
|
||||
// startB: number,
|
||||
// endB: number,
|
||||
// ) {
|
||||
// return getOverlapRange(startA, endA, startB, endB) !== null;
|
||||
// }
|
||||
export type TimeType = {
|
||||
hour: HourNumbers
|
||||
minute: MinuteNumbers
|
||||
second: SecondNumbers
|
||||
}
|
||||
|
||||
// const overlapRange = getOverlapRange(startA, endA, startB, endB);
|
||||
// if (overlapRange) {
|
||||
// console.log(
|
||||
// `Overlap Start: ${new Date(overlapRange.overlapStart).toISOString()}`,
|
||||
// );
|
||||
// console.log(
|
||||
// `Overlap End: ${new Date(overlapRange.overlapEnd).toISOString()}`,
|
||||
// );
|
||||
@Injectable()
|
||||
export class DateTimeUtils {
|
||||
static getOverlapRange(
|
||||
startA: DateTime,
|
||||
endA: DateTime,
|
||||
startB: DateTime,
|
||||
endB: DateTime,
|
||||
): { start: DateTime; end: DateTime } {
|
||||
const overlapStart = DateTime.max(startA, startB)
|
||||
const overlapEnd = DateTime.min(endA, endB)
|
||||
return {
|
||||
start: overlapStart,
|
||||
end: overlapEnd,
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('Is overlap: true');
|
||||
// } else {
|
||||
// console.log('No overlap');
|
||||
// }
|
||||
static isOverlap(
|
||||
startA: DateTime,
|
||||
endA: DateTime,
|
||||
startB: DateTime,
|
||||
endB: DateTime,
|
||||
): boolean {
|
||||
return (
|
||||
this.getOverlapRange(startA, endA, startB, endB).start <
|
||||
this.getOverlapRange(startA, endA, startB, endB).end
|
||||
)
|
||||
}
|
||||
|
||||
static fromIsoString(isoString: string): DateTime {
|
||||
const dateTime = DateTime.fromISO(isoString)
|
||||
if (!dateTime.isValid) {
|
||||
throw new Error('Invalid date time')
|
||||
}
|
||||
return dateTime
|
||||
}
|
||||
|
||||
static fromDate(date: Date): DateTime {
|
||||
const dateTime = DateTime.fromJSDate(date)
|
||||
if (!dateTime.isValid) {
|
||||
throw new Error('Invalid date time')
|
||||
}
|
||||
return dateTime
|
||||
}
|
||||
|
||||
static toIsoString(dateTime: DateTime): string {
|
||||
const isoString = dateTime.toISO()
|
||||
if (!isoString) {
|
||||
throw new Error('Invalid date time')
|
||||
}
|
||||
return isoString
|
||||
}
|
||||
|
||||
static getTodayWithTime(date: DateTime) {
|
||||
const today = DateTime.now().set({
|
||||
hour: date.hour,
|
||||
minute: date.minute,
|
||||
second: date.second,
|
||||
})
|
||||
return today
|
||||
}
|
||||
|
||||
static getSpecificDateWithNowTime(date: DateTime) {
|
||||
return date.set({
|
||||
hour: DateTime.now().hour,
|
||||
minute: DateTime.now().minute,
|
||||
second: DateTime.now().second,
|
||||
})
|
||||
}
|
||||
static getATimeWithDateB(a: DateTime, b: DateTime) {
|
||||
return a.set({
|
||||
year: b.year,
|
||||
month: b.month,
|
||||
day: b.day,
|
||||
hour: a.hour,
|
||||
minute: a.minute,
|
||||
second: a.second,
|
||||
})
|
||||
}
|
||||
|
||||
// example: 12:00:00
|
||||
static toTime(time: string): TimeType {
|
||||
const [hour, minute, second] = time.split(':').map(Number)
|
||||
return {
|
||||
hour: hour as HourNumbers,
|
||||
minute: minute as MinuteNumbers,
|
||||
second: second as SecondNumbers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
src/common/utils/string.utils.ts
Normal file
13
src/common/utils/string.utils.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import _ from 'lodash'
|
||||
|
||||
@Injectable()
|
||||
export class StringUtils {
|
||||
static camelCaseToUpperSneakyCase(str: string) {
|
||||
return _.snakeCase(str).toUpperCase()
|
||||
}
|
||||
|
||||
static sneakyCaseToCamelCase(str: string) {
|
||||
return _.camelCase(str.toLowerCase())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user