From f055d612d3d7855353a00f79a83fbb06eb7e64dc Mon Sep 17 00:00:00 2001 From: avicenan Date: Sun, 19 Apr 2026 19:32:33 +0700 Subject: [PATCH 1/4] fix(schedule): calendar --- src/controllers/schedule.ts | 54 ++++-- src/model/schedule.ts | 377 ++++++++++++++++++++++++++++++------ 2 files changed, 348 insertions(+), 83 deletions(-) diff --git a/src/controllers/schedule.ts b/src/controllers/schedule.ts index 02676e4..eb1a2a4 100644 --- a/src/controllers/schedule.ts +++ b/src/controllers/schedule.ts @@ -137,31 +137,45 @@ export class ScheduleController { #swagger.parameters['limit'] = { in: 'query', required: false, type: 'number' } #swagger.parameters['page'] = { in: 'query', required: false, type: 'number' } #swagger.responses[200] = { - description: 'Schedule calendar resources and events', + description: 'Schedule calendar rooms and events', schema: { status: true, code: 200, message: 'success', data: { - resources: [{ - id: 'room-1:doctor-1', - roomId: 'room-1', - roomName: 'Room 1', - doctorId: 'doctor-1', - doctorName: 'Dr A' - }], - events: [{ - id: 'occ:series-101:2026-03-20', - seriesId: 'series-101', - resourceId: 'room-1:doctor-1', - title: 'Practice', - startAt: '2026-03-20T09:00:00+07:00', - endAt: '2026-03-20T12:00:00+07:00', - isRecurring: true, - isException: false, - status: 'confirmed' - }], - meta: { total_count: 24, page: 1, limit: 20 } + count: 1, + page: 1, + total_count: 1, + list: { + rooms: [{ + id: 'uuid-string', + name: 'string', + code: 'string', + picture: 'string', + location: 'string', + status: 'string', + department: { + name: 'string', + }, + }], + events: [{ + id: 'occ:series-101:2026-03-20', + series_id: 'series-101', + room_id: 'room-1', + room_name: 'Room 1', + doctor_id: 'doctor-1', + doctor_name: 'Doctor 1', + title: 'Practice', + start_at: '2026-03-20T09:00:00+07:00', + end_at: '2026-03-20T12:00:00+07:00', + quota: 10, + used_quota: 5, + left_quota: 5, + is_recurring: true, + is_exception: false, + status: 'confirmed' + }], + } } } } diff --git a/src/model/schedule.ts b/src/model/schedule.ts index b74a61f..28660fd 100644 --- a/src/model/schedule.ts +++ b/src/model/schedule.ts @@ -1,7 +1,9 @@ import { Room, ScheduleException, ScheduleSeries, Status, User } from "entity"; -import { SelectQueryBuilder } from "typeorm"; +import { IsNull, SelectQueryBuilder } from "typeorm"; import CommonHelper from "../helpers/common"; import { OrmHelper } from "../helpers/orm"; +import dayjs from "dayjs"; +import { RoomModel } from "./room"; type RecurrenceInput = { type: "daily" | "weekly" | "monthly"; @@ -57,32 +59,309 @@ 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 = new Date(start); - const endDate = new Date(end); - if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + const startDate = dayjs(start); + const endDate = dayjs(end); + if (!startDate.isValid() || !endDate.isValid()) { throw new Error("Invalid start or end date"); } - const rangeStart = ScheduleModel.startOfDay(startDate); - const rangeEnd = ScheduleModel.endOfDay(endDate); + 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 page = options.page ?? 1; + const limit = options.limit ?? 10; + const offset = (page - 1) * limit; + + const query = await RoomModel.list({ ...filterObj, with_deleted: false }); + const roomQuery = query + .select(["Room.id", "Room.room", "Room.code", "Room.picture", "Room.location", "Room.status", "Department.name"]) + .leftJoin("Room.department", "Department") + .where("Department.name = :departmentName", { departmentName: "Outpatient" }); + const roomTotalCount = await roomQuery + .getCount(); + const rooms = await roomQuery + .orderBy("Room.room", "ASC") + .offset(offset) + .limit(limit) + .getMany(); + const roomIds = rooms.map((room) => room.id); + 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 seriesList = await seriesQuery + .where("ScheduleSeries.room_id IN (:...roomIds)", { roomIds }) + .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") + .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") @@ -98,7 +377,7 @@ export class ScheduleModel { const exceptionMap = new Map(); for (const ex of exceptions) { - const key = `${ex.series_id}:${ScheduleModel.formatYmd(new Date(ex.occurrence_date))}`; + const key = `${ex.series_id}:${dayjs(ex.occurrence_date).format("YYYY-MM-DD")}`; exceptionMap.set(key, ex); } @@ -107,7 +386,7 @@ export class ScheduleModel { for (const series of seriesList) { const occurrenceDates = ScheduleModel.expandSeriesOccurrences(series, rangeStart, rangeEnd); for (const occurrenceDate of occurrenceDates) { - const occurrenceYmd = ScheduleModel.formatYmd(occurrenceDate); + const occurrenceYmd = dayjs(occurrenceDate).format("YYYY-MM-DD"); const exception = exceptionMap.get(`${series.id}:${occurrenceYmd}`); if (exception && exception.is_cancelled) continue; @@ -122,68 +401,40 @@ export class ScheduleModel { const roomId = room?.id || null; const doctorId = doctor?.id || null; - 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, + series_id: series.id, title: eventTitle, - startAt: eventStart.toISOString(), - endAt: eventEnd.toISOString(), + room_id: roomId, + room_name: roomName, + doctor_id: doctorId, + doctor_name: doctorName, + start_at: eventStart.toISOString(), + end_at: eventEnd.toISOString(), quota: series.quota ?? null, - usedQuota: usageMap.get(`${series.id}:${occurrenceYmd}`) || 0, - leftQuota: + 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, - isRecurring: !!series.recurrence_rule, - isException: !!exception, + is_recurring: !!series.recurrence_rule, + is_exception: !!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, - }, + count: rooms.length, + page: page, + total_count: roomTotalCount, + list: { + rooms: rooms, + events: allEvents, + } }; } From 9099aabce8826dca2d6eba059fac956fd1fff6bb Mon Sep 17 00:00:00 2001 From: avicenan Date: Sun, 19 Apr 2026 20:32:54 +0700 Subject: [PATCH 2/4] fix(schedule): calendar --- src/model/schedule.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/model/schedule.ts b/src/model/schedule.ts index 28660fd..69fbd27 100644 --- a/src/model/schedule.ts +++ b/src/model/schedule.ts @@ -427,14 +427,16 @@ export class ScheduleModel { } } + for (const room of rooms) { + const roomEvents = allEvents.filter((event) => event.room_id === room.id); + room.events = roomEvents; + } + return { count: rooms.length, page: page, total_count: roomTotalCount, - list: { - rooms: rooms, - events: allEvents, - } + list: rooms, }; } From 1bd0f081d7a491545fd7949bab67f8308c9b38eb Mon Sep 17 00:00:00 2001 From: avicenan Date: Sun, 19 Apr 2026 22:24:36 +0700 Subject: [PATCH 3/4] 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", + }); + } } } From e88c6cf0db031647137da6230c992225c44443b4 Mon Sep 17 00:00:00 2001 From: avicenan Date: Sun, 19 Apr 2026 23:14:09 +0700 Subject: [PATCH 4/4] fix(schedule): calendar --- src/model/schedule.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/model/schedule.ts b/src/model/schedule.ts index 8bd231b..e60241b 100644 --- a/src/model/schedule.ts +++ b/src/model/schedule.ts @@ -134,8 +134,8 @@ export class ScheduleModel { 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 eventStart = exception?.override_start_at ? exception.override_start_at : defaultStart; + const eventEnd = exception?.override_end_at ? exception.override_end_at : defaultEnd; const eventTitle = exception?.override_title || series.title; const roomId = room?.id || null; @@ -151,8 +151,8 @@ export class ScheduleModel { room_name: roomName, doctor_id: doctorId, doctor_name: doctorName, - start_at: eventStart.toISOString(), - end_at: eventEnd.toISOString(), + start_at: eventStart, + end_at: eventEnd, quota: series.quota ?? null, used_quota: usageMap.get(`${series.id}:${occurrenceYmd}`) || 0, left_quota: @@ -462,8 +462,8 @@ export class ScheduleModel { }; } - private static combineDateAndTime(ymd: string, hhmmss: string): Date { - return new Date(`${ymd}T${hhmmss}`); + private static combineDateAndTime(ymd: string, hhmmss: string) { + return `${ymd} ${hhmmss}`; } private static startOfDay(date: Date): Date {