From 1bd0f081d7a491545fd7949bab67f8308c9b38eb Mon Sep 17 00:00:00 2001 From: avicenan Date: Sun, 19 Apr 2026 22:24:36 +0700 Subject: [PATCH] fix(schedule): calendar --- src/model/schedule.ts | 380 +++++++----------------------------------- 1 file changed, 60 insertions(+), 320 deletions(-) diff --git a/src/model/schedule.ts b/src/model/schedule.ts index 69fbd27..8bd231b 100644 --- a/src/model/schedule.ts +++ b/src/model/schedule.ts @@ -59,266 +59,6 @@ export class ScheduleModel { return query; } - // static async getScheduleCalendarRange(start: string, end: string, filterObj = {}, options: CalendarQueryOptions = {}): Promise { - // const startDate = new Date(start); - // const endDate = new Date(end); - // if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { - // throw new Error("Invalid start or end date"); - // } - - // const rangeStart = ScheduleModel.startOfDay(startDate); - // const rangeEnd = ScheduleModel.endOfDay(endDate); - // if (rangeStart > rangeEnd) { - // throw new Error("Start date must be before end date"); - // } - - // const seriesQuery = await ScheduleModel.listSeries(filterObj); - // seriesQuery - // .andWhere("ScheduleSeries.start_date <= :rangeEndDate", { rangeEndDate: ScheduleModel.formatYmd(rangeEnd) }) - // .andWhere("(ScheduleSeries.until_date IS NULL OR ScheduleSeries.until_date >= :rangeStartDate)", { - // rangeStartDate: ScheduleModel.formatYmd(rangeStart), - // }) - // .andWhere("ScheduleSeries.deleted_at IS NULL"); - - // const seriesList: ScheduleSeries[] = await seriesQuery.getMany(); - // if (seriesList.length === 0) return { resources: [], events: [] }; - - // const seriesIds = seriesList.map((series) => series.id); - // const usageMap = await ScheduleModel.getDailyUsedQuotaMap(seriesIds, rangeStart, rangeEnd); - // const exceptionRepo = OrmHelper.DB.getRepository(ScheduleException); - // const exceptions = await exceptionRepo - // .createQueryBuilder("ScheduleException") - // .leftJoinAndSelect("ScheduleException.override_room", "OverrideRoom") - // .leftJoinAndSelect("ScheduleException.override_doctor", "OverrideDoctor") - // .where("ScheduleException.series_id IN (:...seriesIds)", { seriesIds }) - // .andWhere("ScheduleException.occurrence_date BETWEEN :rangeStart AND :rangeEnd", { - // rangeStart: ScheduleModel.formatYmd(rangeStart), - // rangeEnd: ScheduleModel.formatYmd(rangeEnd), - // }) - // .andWhere("ScheduleException.deleted_at IS NULL") - // .getMany(); - - // const exceptionMap = new Map(); - // for (const ex of exceptions) { - // const key = `${ex.series_id}:${ScheduleModel.formatYmd(new Date(ex.occurrence_date))}`; - // exceptionMap.set(key, ex); - // } - - // const allEvents: any[] = []; - - // for (const series of seriesList) { - // const occurrenceDates = ScheduleModel.expandSeriesOccurrences(series, rangeStart, rangeEnd); - // for (const occurrenceDate of occurrenceDates) { - // const occurrenceYmd = ScheduleModel.formatYmd(occurrenceDate); - // const exception = exceptionMap.get(`${series.id}:${occurrenceYmd}`); - // if (exception && exception.is_cancelled) continue; - - // const defaultStart = ScheduleModel.combineDateAndTime(occurrenceYmd, series.start_time); - // const defaultEnd = ScheduleModel.combineDateAndTime(occurrenceYmd, series.end_time); - - // // const doctor = exception?.override_doctor ?? series.doctor; - // const room = exception?.override_room ?? series.room; - // const eventStart = exception?.override_start_at ? new Date(exception.override_start_at) : defaultStart; - // const eventEnd = exception?.override_end_at ? new Date(exception.override_end_at) : defaultEnd; - // const eventTitle = exception?.override_title || series.title; - - // const roomId = room?.id || null; - // // const doctorId = doctor?.id || null; - // const resourceId = `${roomId || "unknown-room"}`; - // // const resourceId = `${roomId || "unknown-room"}:${doctorId || "unknown-doctor"}`; - // const roomName = (room as any)?.room || (room as any)?.name || (room as any)?.code || ""; - // // const doctorName = (doctor as any)?.name || (doctor as any)?.fullname || (doctor as any)?.username || ""; - - // allEvents.push({ - // id: `occ:${series.id}:${occurrenceYmd}`, - // seriesId: series.id, - // resourceId, - // title: eventTitle, - // startAt: eventStart.toISOString(), - // endAt: eventEnd.toISOString(), - // quota: series.quota ?? null, - // usedQuota: usageMap.get(`${series.id}:${occurrenceYmd}`) || 0, - // leftQuota: - // typeof series.quota === "number" - // ? Math.max(0, series.quota - (usageMap.get(`${series.id}:${occurrenceYmd}`) || 0)) - // : null, - // isRecurring: !!series.recurrence_rule, - // isException: !!exception, - // status: "confirmed", - // _resource: { - // id: resourceId, - // roomId, - // roomName, - // // doctorId, - // // doctorName, - // }, - // }); - // } - // } - - // const allResourcesMap = new Map(); - // for (const event of allEvents) { - // allResourcesMap.set(event._resource.id, event._resource); - // } - // const allResources = [...allResourcesMap.values()].sort((a, b) => a.id.localeCompare(b.id)); - // const totalCount = allResources.length; - // const page = options.page ?? 1; - // const limit = options.limit ?? totalCount; - // const offset = (page - 1) * limit; - // const pagedResources = allResources.slice(offset, offset + limit); - // const allowedResourceIds = new Set(pagedResources.map((resource) => resource.id)); - - // const pagedEvents = allEvents - // .filter((event) => allowedResourceIds.has(event.resourceId)) - // .sort((a, b) => { - // if (a.startAt === b.startAt) return a.id.localeCompare(b.id); - // return a.startAt.localeCompare(b.startAt); - // }) - // .map((event) => { - // const { _resource, ...cleanEvent } = event; - // return cleanEvent; - // }); - - // return { - // resources: pagedResources, - // events: pagedEvents, - // meta: { - // total_count: totalCount, - // page, - // limit, - // }, - // }; - // } - - // return resource as rooms and all the events with relation of room and doctor - // static async getScheduleCalendarRange(start: string, end: string, filterObj = {}, options: CalendarQueryOptions = {}): Promise { - // const startDate = dayjs(start); - // const endDate = dayjs(end); - // if (!startDate.isValid() || !endDate.isValid()) { - // throw new Error("Invalid start or end date"); - // } - - // const rangeStart = startDate.startOf("day").toDate(); - // const rangeEnd = endDate.endOf("day").toDate(); - // if (rangeStart > rangeEnd) { - // throw new Error("Start date must be before end date"); - // } - - // const seriesQuery = await ScheduleModel.listSeries(filterObj); - // seriesQuery - // .andWhere("ScheduleSeries.start_date <= :rangeEndDate", { rangeEndDate: dayjs(rangeEnd).format("YYYY-MM-DD") }) - // .andWhere("(ScheduleSeries.until_date IS NULL OR ScheduleSeries.until_date >= :rangeStartDate)", { - // rangeStartDate: dayjs(rangeStart).format("YYYY-MM-DD"), - // }) - // .andWhere("ScheduleSeries.deleted_at IS NULL"); - - // const seriesList: ScheduleSeries[] = await seriesQuery.getMany(); - // if (seriesList.length === 0) return { rooms: [], events: [] }; - - // const seriesIds = seriesList.map((series) => series.id); - // const usageMap = await ScheduleModel.getDailyUsedQuotaMap(seriesIds, rangeStart, rangeEnd); - // const exceptionRepo = OrmHelper.DB.getRepository(ScheduleException); - // const exceptions = await exceptionRepo - // .createQueryBuilder("ScheduleException") - // .leftJoinAndSelect("ScheduleException.override_room", "OverrideRoom") - // .leftJoinAndSelect("ScheduleException.override_doctor", "OverrideDoctor") - // .where("ScheduleException.series_id IN (:...seriesIds)", { seriesIds }) - // .andWhere("ScheduleException.occurrence_date BETWEEN :rangeStart AND :rangeEnd", { - // rangeStart: ScheduleModel.formatYmd(rangeStart), - // rangeEnd: ScheduleModel.formatYmd(rangeEnd), - // }) - // .andWhere("ScheduleException.deleted_at IS NULL") - // .getMany(); - - // const exceptionMap = new Map(); - // for (const ex of exceptions) { - // const key = `${ex.series_id}:${dayjs(ex.occurrence_date).format("YYYY-MM-DD")}`; - // exceptionMap.set(key, ex); - // } - - // const allEvents: any[] = []; - - // for (const series of seriesList) { - // const occurrenceDates = ScheduleModel.expandSeriesOccurrences(series, rangeStart, rangeEnd); - // for (const occurrenceDate of occurrenceDates) { - // const occurrenceYmd = dayjs(occurrenceDate).format("YYYY-MM-DD"); - // const exception = exceptionMap.get(`${series.id}:${occurrenceYmd}`); - // if (exception && exception.is_cancelled) continue; - - // const defaultStart = ScheduleModel.combineDateAndTime(occurrenceYmd, series.start_time); - // const defaultEnd = ScheduleModel.combineDateAndTime(occurrenceYmd, series.end_time); - - // const doctor = exception?.override_doctor ?? series.doctor; - // const room = exception?.override_room ?? series.room; - // const eventStart = exception?.override_start_at ? new Date(exception.override_start_at) : defaultStart; - // const eventEnd = exception?.override_end_at ? new Date(exception.override_end_at) : defaultEnd; - // const eventTitle = exception?.override_title || series.title; - - // const roomId = room?.id || null; - // const doctorId = doctor?.id || null; - // const roomName = (room as any)?.room || (room as any)?.name || (room as any)?.code || ""; - // const doctorName = (doctor as any)?.name || (doctor as any)?.fullname || (doctor as any)?.username || ""; - - // allEvents.push({ - // id: `occ:${series.id}:${occurrenceYmd}`, - // seriesId: series.id, - // roomId, - // doctorId, - // doctorName, - // title: eventTitle, - // startAt: eventStart.toISOString(), - // endAt: eventEnd.toISOString(), - // quota: series.quota ?? null, - // usedQuota: usageMap.get(`${series.id}:${occurrenceYmd}`) || 0, - // leftQuota: - // typeof series.quota === "number" - // ? Math.max(0, series.quota - (usageMap.get(`${series.id}:${occurrenceYmd}`) || 0)) - // : null, - // isRecurring: !!series.recurrence_rule, - // isException: !!exception, - // status: "confirmed", - // _room: { - // id: roomId, - // name: roomName, - // }, - // }); - // } - // } - - // const allRoomsMap = new Map(); - // for (const event of allEvents) { - // allRoomsMap.set(event._room.id, event._room); - // } - // const allRooms = [...allRoomsMap.values()].sort((a, b) => a.id.localeCompare(b.id)); - // const totalCount = allRooms.length; - // const page = options.page ?? 1; - // const limit = options.limit ?? totalCount; - // const offset = (page - 1) * limit; - // const pagedRooms = allRooms.slice(offset, offset + limit); - // const allowedRoomIds = new Set(pagedRooms.map((room) => room.id)); - - // const pagedEvents = allEvents - // .filter((event) => allowedRoomIds.has(event.roomId)) - // .sort((a, b) => { - // if (a.startAt === b.startAt) return a.id.localeCompare(b.id); - // return a.startAt.localeCompare(b.startAt); - // }) - // .map((event) => { - // const { _room, ...cleanEvent } = event; - // return cleanEvent; - // }); - - // return { - // rooms: pagedRooms, - // events: pagedEvents, - // meta: { - // total_count: totalCount, - // page, - // limit, - // }, - // }; - // } - static async getScheduleCalendarRange(start: string, end: string, filterObj = {}, options: CalendarQueryOptions = {}): Promise { const startDate = dayjs(start); const endDate = dayjs(end); @@ -358,72 +98,72 @@ export class ScheduleModel { .andWhere("ScheduleSeries.deleted_at IS NULL") .getMany(); - if (seriesList.length === 0) return { rooms: rooms, events: [], meta: { total_count: rooms.length, page, limit } }; - const seriesIds = seriesList.map((series) => series.id); - const usageMap = await ScheduleModel.getDailyUsedQuotaMap(seriesIds, rangeStart, rangeEnd); - - const exceptionRepo = OrmHelper.DB.getRepository(ScheduleException); - const exceptions = await exceptionRepo - .createQueryBuilder("ScheduleException") - .leftJoinAndSelect("ScheduleException.override_room", "OverrideRoom") - .leftJoinAndSelect("ScheduleException.override_doctor", "OverrideDoctor") - .where("ScheduleException.series_id IN (:...seriesIds)", { seriesIds }) - .andWhere("ScheduleException.occurrence_date BETWEEN :rangeStart AND :rangeEnd", { - rangeStart: ScheduleModel.formatYmd(rangeStart), - rangeEnd: ScheduleModel.formatYmd(rangeEnd), - }) - .andWhere("ScheduleException.deleted_at IS NULL") - .getMany(); - - const exceptionMap = new Map(); - for (const ex of exceptions) { - const key = `${ex.series_id}:${dayjs(ex.occurrence_date).format("YYYY-MM-DD")}`; - exceptionMap.set(key, ex); - } - const allEvents: any[] = []; + if (seriesList.length > 0) { + const seriesIds = seriesList.map((series) => series.id); + const usageMap = await ScheduleModel.getDailyUsedQuotaMap(seriesIds, rangeStart, rangeEnd); - for (const series of seriesList) { - const occurrenceDates = ScheduleModel.expandSeriesOccurrences(series, rangeStart, rangeEnd); - for (const occurrenceDate of occurrenceDates) { - const occurrenceYmd = dayjs(occurrenceDate).format("YYYY-MM-DD"); - const exception = exceptionMap.get(`${series.id}:${occurrenceYmd}`); - if (exception && exception.is_cancelled) continue; + const exceptionRepo = OrmHelper.DB.getRepository(ScheduleException); + const exceptions = await exceptionRepo + .createQueryBuilder("ScheduleException") + .leftJoinAndSelect("ScheduleException.override_room", "OverrideRoom") + .leftJoinAndSelect("ScheduleException.override_doctor", "OverrideDoctor") + .where("ScheduleException.series_id IN (:...seriesIds)", { seriesIds }) + .andWhere("ScheduleException.occurrence_date BETWEEN :rangeStart AND :rangeEnd", { + rangeStart: ScheduleModel.formatYmd(rangeStart), + rangeEnd: ScheduleModel.formatYmd(rangeEnd), + }) + .andWhere("ScheduleException.deleted_at IS NULL") + .getMany(); - const defaultStart = ScheduleModel.combineDateAndTime(occurrenceYmd, series.start_time); - const defaultEnd = ScheduleModel.combineDateAndTime(occurrenceYmd, series.end_time); + const exceptionMap = new Map(); + for (const ex of exceptions) { + const key = `${ex.series_id}:${dayjs(ex.occurrence_date).format("YYYY-MM-DD")}`; + exceptionMap.set(key, ex); + } - const doctor = exception?.override_doctor ?? series.doctor; - const room = exception?.override_room ?? series.room; - const eventStart = exception?.override_start_at ? new Date(exception.override_start_at) : defaultStart; - const eventEnd = exception?.override_end_at ? new Date(exception.override_end_at) : defaultEnd; - const eventTitle = exception?.override_title || series.title; + for (const series of seriesList) { + const occurrenceDates = ScheduleModel.expandSeriesOccurrences(series, rangeStart, rangeEnd); + for (const occurrenceDate of occurrenceDates) { + const occurrenceYmd = dayjs(occurrenceDate).format("YYYY-MM-DD"); + const exception = exceptionMap.get(`${series.id}:${occurrenceYmd}`); + if (exception && exception.is_cancelled) continue; - const roomId = room?.id || null; - const doctorId = doctor?.id || null; - const roomName = (room as any)?.room || (room as any)?.name || (room as any)?.code || ""; - const doctorName = (doctor as any)?.name || (doctor as any)?.fullname || (doctor as any)?.username || ""; + const defaultStart = ScheduleModel.combineDateAndTime(occurrenceYmd, series.start_time); + const defaultEnd = ScheduleModel.combineDateAndTime(occurrenceYmd, series.end_time); - allEvents.push({ - id: `occ:${series.id}:${occurrenceYmd}`, - series_id: series.id, - title: eventTitle, - room_id: roomId, - room_name: roomName, - doctor_id: doctorId, - doctor_name: doctorName, - start_at: eventStart.toISOString(), - end_at: eventEnd.toISOString(), - quota: series.quota ?? null, - used_quota: usageMap.get(`${series.id}:${occurrenceYmd}`) || 0, - left_quota: - typeof series.quota === "number" - ? Math.max(0, series.quota - (usageMap.get(`${series.id}:${occurrenceYmd}`) || 0)) - : null, - is_recurring: !!series.recurrence_rule, - is_exception: !!exception, - status: "confirmed", - }); + const doctor = exception?.override_doctor ?? series.doctor; + const room = exception?.override_room ?? series.room; + const eventStart = exception?.override_start_at ? new Date(exception.override_start_at) : defaultStart; + const eventEnd = exception?.override_end_at ? new Date(exception.override_end_at) : defaultEnd; + const eventTitle = exception?.override_title || series.title; + + const roomId = room?.id || null; + const doctorId = doctor?.id || null; + const roomName = (room as any)?.room || (room as any)?.name || (room as any)?.code || ""; + const doctorName = (doctor as any)?.name || (doctor as any)?.fullname || (doctor as any)?.username || ""; + + allEvents.push({ + id: `occ:${series.id}:${occurrenceYmd}`, + series_id: series.id, + title: eventTitle, + room_id: roomId, + room_name: roomName, + doctor_id: doctorId, + doctor_name: doctorName, + start_at: eventStart.toISOString(), + end_at: eventEnd.toISOString(), + quota: series.quota ?? null, + used_quota: usageMap.get(`${series.id}:${occurrenceYmd}`) || 0, + left_quota: + typeof series.quota === "number" + ? Math.max(0, series.quota - (usageMap.get(`${series.id}:${occurrenceYmd}`) || 0)) + : null, + is_recurring: !!series.recurrence_rule, + is_exception: !!exception, + status: "confirmed", + }); + } } }