From e872fc8e4d72e4cef6793593097f507755c9cd07 Mon Sep 17 00:00:00 2001 From: avicenan Date: Wed, 25 Mar 2026 10:10:31 +0700 Subject: [PATCH] fixing timezone --- src/model/schedule.ts | 762 +++++++++++++++++++++--------------------- 1 file changed, 381 insertions(+), 381 deletions(-) diff --git a/src/model/schedule.ts b/src/model/schedule.ts index e9ece82..e980b77 100644 --- a/src/model/schedule.ts +++ b/src/model/schedule.ts @@ -4,455 +4,455 @@ import CommonHelper from "../helpers/common"; import { OrmHelper } from "../helpers/orm"; type RecurrenceInput = { - type: "daily" | "weekly" | "monthly"; - interval?: number; - daysOfWeek?: string[]; + type: "daily" | "weekly" | "monthly"; + interval?: number; + daysOfWeek?: string[]; }; type ScheduleSeriesPayload = { - roomId: string; - doctorId: string; - title: string; - timezone: string; - startDate: string; - untilDate?: string | null; - startTimeLocal: string; - endTimeLocal: string; - recurrence?: RecurrenceInput | null; + roomId: string; + doctorId: string; + title: string; + timezone: string; + startDate: string; + untilDate?: string | null; + startTimeLocal: string; + endTimeLocal: string; + recurrence?: RecurrenceInput | null; }; type ScheduleExceptionPayload = { - occurrenceDate: string; - isCancelled?: boolean; - overrideRoomId?: string | null; - overrideDoctorId?: string | null; - overrideTitle?: string | null; - overrideStartAt?: string | null; - overrideEndAt?: string | null; + occurrenceDate: string; + isCancelled?: boolean; + overrideRoomId?: string | null; + overrideDoctorId?: string | null; + overrideTitle?: string | null; + overrideStartAt?: string | null; + overrideEndAt?: string | null; }; type CalendarQueryOptions = { - limit?: number; - page?: number; + limit?: number; + page?: number; }; export class ScheduleModel { - static async listSeries(filterObj = {}): Promise> { - const repo = OrmHelper.DB.getRepository(ScheduleSeries); - let whereAttr: string[] = []; - let whereVal: any = {}; - if (filterObj && Object.keys(filterObj).length > 0) { - const filterResult = CommonHelper.handleQueryFilter(filterObj); - whereAttr = [...whereAttr, ...filterResult.whereAttr]; - whereVal = { ...whereVal, ...filterResult.whereVal }; - } + static async listSeries(filterObj = {}): Promise> { + const repo = OrmHelper.DB.getRepository(ScheduleSeries); + let whereAttr: string[] = []; + let whereVal: any = {}; + if (filterObj && Object.keys(filterObj).length > 0) { + const filterResult = CommonHelper.handleQueryFilter(filterObj); + whereAttr = [...whereAttr, ...filterResult.whereAttr]; + whereVal = { ...whereVal, ...filterResult.whereVal }; + } - let query = repo - .createQueryBuilder("ScheduleSeries") - .leftJoinAndSelect("ScheduleSeries.doctor", "Doctor") - .leftJoinAndSelect("ScheduleSeries.room", "Room"); - if (whereAttr.length !== 0) { - query = query.where(whereAttr.join(" and "), whereVal); - } - return query; - } + let query = repo + .createQueryBuilder("ScheduleSeries") + .leftJoinAndSelect("ScheduleSeries.doctor", "Doctor") + .leftJoinAndSelect("ScheduleSeries.room", "Room"); + if (whereAttr.length !== 0) { + query = query.where(whereAttr.join(" and "), whereVal); + } + 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"); - } + 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 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 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: ScheduleSeries[] = await seriesQuery.getMany(); + if (seriesList.length === 0) return { resources: [], events: [] }; - const seriesIds = seriesList.map((series) => series.id); - 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 seriesIds = seriesList.map((series) => series.id); + 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 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[] = []; + 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; + 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_local); - const defaultEnd = ScheduleModel.combineDateAndTime(occurrenceYmd, series.end_time_local); + const defaultStart = ScheduleModel.combineDateAndTime(occurrenceYmd, series.start_time_local); + const defaultEnd = ScheduleModel.combineDateAndTime(occurrenceYmd, series.end_time_local); - 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 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"}:${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 || ""; + 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, - title: eventTitle, - startAt: eventStart.toISOString(), - endAt: eventEnd.toISOString(), - isRecurring: !!series.recurrence_rule, - isException: !!exception, - status: "confirmed", - _resource: { - id: resourceId, - roomId, - roomName, - doctorId, - doctorName, - }, - }); - } - } + allEvents.push({ + id: `occ:${series.id}:${occurrenceYmd}`, + seriesId: series.id, + resourceId, + title: eventTitle, + startAt: eventStart.toLocaleString('en-US', { timeZone: series.timezone }), + endAt: eventEnd.toLocaleString('en-US', { timeZone: series.timezone }), + 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 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; - }); + 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 { + resources: pagedResources, + events: pagedEvents, + meta: { + total_count: totalCount, + page, + limit, + }, + }; + } - static async getScheduleOptions(filterObj = {}): Promise { - const query = await ScheduleModel.listSeries(filterObj); - query.andWhere("ScheduleSeries.deleted_at IS NULL"); - const seriesList: ScheduleSeries[] = await query.getMany(); + static async getScheduleOptions(filterObj = {}): Promise { + const query = await ScheduleModel.listSeries(filterObj); + query.andWhere("ScheduleSeries.deleted_at IS NULL"); + const seriesList: ScheduleSeries[] = await query.getMany(); - const doctorsMap = new Map(); - const roomsMap = new Map(); + const doctorsMap = new Map(); + const roomsMap = new Map(); - for (const series of seriesList) { - if (series.doctor?.id) { - doctorsMap.set(series.doctor.id, { - id: series.doctor.id, - title: (series.doctor as any).name || (series.doctor as any).username || "Doctor", - }); - } - if (series.room?.id) { - roomsMap.set(series.room.id, { - id: series.room.id, - title: (series.room as any).room || (series.room as any).code || "Room", - }); - } - } + for (const series of seriesList) { + if (series.doctor?.id) { + doctorsMap.set(series.doctor.id, { + id: series.doctor.id, + title: (series.doctor as any).name || (series.doctor as any).username || "Doctor", + }); + } + if (series.room?.id) { + roomsMap.set(series.room.id, { + id: series.room.id, + title: (series.room as any).room || (series.room as any).code || "Room", + }); + } + } - return { - doctors: [...doctorsMap.values()], - rooms: [...roomsMap.values()], - }; - } + return { + doctors: [...doctorsMap.values()], + rooms: [...roomsMap.values()], + }; + } - static async getSeriesById(id: string): Promise { - const series = await OrmHelper.DB.getRepository(ScheduleSeries) - .createQueryBuilder("ScheduleSeries") - .leftJoinAndSelect("ScheduleSeries.doctor", "Doctor") - .leftJoinAndSelect("ScheduleSeries.room", "Room") - .where("ScheduleSeries.id = :id", { id }) - .andWhere("ScheduleSeries.deleted_at IS NULL") - .getOne(); + static async getSeriesById(id: string): Promise { + const series = await OrmHelper.DB.getRepository(ScheduleSeries) + .createQueryBuilder("ScheduleSeries") + .leftJoinAndSelect("ScheduleSeries.doctor", "Doctor") + .leftJoinAndSelect("ScheduleSeries.room", "Room") + .where("ScheduleSeries.id = :id", { id }) + .andWhere("ScheduleSeries.deleted_at IS NULL") + .getOne(); - if (!series) throw new Error("Schedule series not found"); + if (!series) throw new Error("Schedule series not found"); - const exceptions = await OrmHelper.DB.getRepository(ScheduleException) - .createQueryBuilder("ScheduleException") - .leftJoinAndSelect("ScheduleException.override_room", "OverrideRoom") - .leftJoinAndSelect("ScheduleException.override_doctor", "OverrideDoctor") - .where("ScheduleException.series_id = :id", { id }) - .andWhere("ScheduleException.deleted_at IS NULL") - .orderBy("ScheduleException.occurrence_date", "ASC") - .getMany(); + const exceptions = await OrmHelper.DB.getRepository(ScheduleException) + .createQueryBuilder("ScheduleException") + .leftJoinAndSelect("ScheduleException.override_room", "OverrideRoom") + .leftJoinAndSelect("ScheduleException.override_doctor", "OverrideDoctor") + .where("ScheduleException.series_id = :id", { id }) + .andWhere("ScheduleException.deleted_at IS NULL") + .orderBy("ScheduleException.occurrence_date", "ASC") + .getMany(); - return { series, exceptions }; - } + return { series, exceptions }; + } - static async createSeries(payload: ScheduleSeriesPayload, actor: string): Promise { - const room = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.roomId } }); - if (!room) throw new Error("Room not found"); + static async createSeries(payload: ScheduleSeriesPayload, actor: string): Promise { + const room = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.roomId } }); + if (!room) throw new Error("Room not found"); - const doctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.doctorId } }); - if (!doctor) throw new Error("Doctor not found"); + const doctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.doctorId } }); + if (!doctor) throw new Error("Doctor not found"); - const series = new ScheduleSeries(); - series.room_id = payload.roomId; - series.doctor_id = payload.doctorId; - series.room = room; - series.doctor = doctor; - series.title = payload.title; - series.timezone = payload.timezone; - series.start_date = new Date(payload.startDate); - series.until_date = payload.untilDate ? new Date(payload.untilDate) : null; - series.start_time_local = payload.startTimeLocal; - series.end_time_local = payload.endTimeLocal; - series.recurrence_rule = ScheduleModel.recurrenceToRRule(payload.recurrence || null); - series.status = Status.Active; - series.created_by = actor; - series.updated_by = actor; + const series = new ScheduleSeries(); + series.room_id = payload.roomId; + series.doctor_id = payload.doctorId; + series.room = room; + series.doctor = doctor; + series.title = payload.title; + series.timezone = payload.timezone; + series.start_date = new Date(payload.startDate); + series.until_date = payload.untilDate ? new Date(payload.untilDate) : null; + series.start_time_local = payload.startTimeLocal; + series.end_time_local = payload.endTimeLocal; + series.recurrence_rule = ScheduleModel.recurrenceToRRule(payload.recurrence || null); + series.status = Status.Active; + series.created_by = actor; + series.updated_by = actor; - return OrmHelper.DB.manager.save(series); - } + return OrmHelper.DB.manager.save(series); + } - static async updateSeries(id: string, payload: ScheduleSeriesPayload, actor: string): Promise { - const series = await OrmHelper.DB.getRepository(ScheduleSeries) - .createQueryBuilder("ScheduleSeries") - .where("ScheduleSeries.id = :id", { id }) - .andWhere("ScheduleSeries.deleted_at IS NULL") - .getOne(); - if (!series) throw new Error("Schedule series not found"); + static async updateSeries(id: string, payload: ScheduleSeriesPayload, actor: string): Promise { + const series = await OrmHelper.DB.getRepository(ScheduleSeries) + .createQueryBuilder("ScheduleSeries") + .where("ScheduleSeries.id = :id", { id }) + .andWhere("ScheduleSeries.deleted_at IS NULL") + .getOne(); + if (!series) throw new Error("Schedule series not found"); - const room = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.roomId } }); - if (!room) throw new Error("Room not found"); + const room = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.roomId } }); + if (!room) throw new Error("Room not found"); - const doctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.doctorId } }); - if (!doctor) throw new Error("Doctor not found"); + const doctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.doctorId } }); + if (!doctor) throw new Error("Doctor not found"); - series.room_id = payload.roomId; - series.doctor_id = payload.doctorId; - series.room = room; - series.doctor = doctor; - series.title = payload.title; - series.timezone = payload.timezone; - series.start_date = new Date(payload.startDate); - series.until_date = payload.untilDate ? new Date(payload.untilDate) : null; - series.start_time_local = payload.startTimeLocal; - series.end_time_local = payload.endTimeLocal; - series.recurrence_rule = ScheduleModel.recurrenceToRRule(payload.recurrence || null); - series.updated_by = actor; + series.room_id = payload.roomId; + series.doctor_id = payload.doctorId; + series.room = room; + series.doctor = doctor; + series.title = payload.title; + series.timezone = payload.timezone; + series.start_date = new Date(payload.startDate); + series.until_date = payload.untilDate ? new Date(payload.untilDate) : null; + series.start_time_local = payload.startTimeLocal; + series.end_time_local = payload.endTimeLocal; + series.recurrence_rule = ScheduleModel.recurrenceToRRule(payload.recurrence || null); + series.updated_by = actor; - return OrmHelper.DB.manager.save(series); - } + return OrmHelper.DB.manager.save(series); + } - static async createSeriesException(seriesId: string, payload: ScheduleExceptionPayload, actor: string): Promise { - const series = await OrmHelper.DB.getRepository(ScheduleSeries).findOne({ where: { id: seriesId } }); - if (!series) throw new Error("Schedule series not found"); + static async createSeriesException(seriesId: string, payload: ScheduleExceptionPayload, actor: string): Promise { + const series = await OrmHelper.DB.getRepository(ScheduleSeries).findOne({ where: { id: seriesId } }); + if (!series) throw new Error("Schedule series not found"); - let overrideRoom: Room = null; - if (payload.overrideRoomId) { - overrideRoom = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.overrideRoomId } }); - if (!overrideRoom) throw new Error("Override room not found"); - } + let overrideRoom: Room = null; + if (payload.overrideRoomId) { + overrideRoom = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.overrideRoomId } }); + if (!overrideRoom) throw new Error("Override room not found"); + } - let overrideDoctor: User = null; - if (payload.overrideDoctorId) { - overrideDoctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.overrideDoctorId } }); - if (!overrideDoctor) throw new Error("Override doctor not found"); - } + let overrideDoctor: User = null; + if (payload.overrideDoctorId) { + overrideDoctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.overrideDoctorId } }); + if (!overrideDoctor) throw new Error("Override doctor not found"); + } - const occurrenceDate = new Date(payload.occurrenceDate); - const occurrenceYmd = ScheduleModel.formatYmd(occurrenceDate); + const occurrenceDate = new Date(payload.occurrenceDate); + const occurrenceYmd = ScheduleModel.formatYmd(occurrenceDate); - let exception = await OrmHelper.DB.getRepository(ScheduleException) - .createQueryBuilder("ScheduleException") - .where("ScheduleException.series_id = :seriesId", { seriesId }) - .andWhere("ScheduleException.occurrence_date = :occurrenceDate", { occurrenceDate: occurrenceYmd }) - .andWhere("ScheduleException.deleted_at IS NULL") - .getOne(); + let exception = await OrmHelper.DB.getRepository(ScheduleException) + .createQueryBuilder("ScheduleException") + .where("ScheduleException.series_id = :seriesId", { seriesId }) + .andWhere("ScheduleException.occurrence_date = :occurrenceDate", { occurrenceDate: occurrenceYmd }) + .andWhere("ScheduleException.deleted_at IS NULL") + .getOne(); - if (!exception) { - exception = new ScheduleException(); - exception.series_id = seriesId; - exception.series = series; - exception.occurrence_date = occurrenceDate; - exception.created_by = actor; - } + if (!exception) { + exception = new ScheduleException(); + exception.series_id = seriesId; + exception.series = series; + exception.occurrence_date = occurrenceDate; + exception.created_by = actor; + } - exception.is_cancelled = !!payload.isCancelled; - exception.override_room_id = payload.overrideRoomId || null; - exception.override_doctor_id = payload.overrideDoctorId || null; - exception.override_room = overrideRoom; - exception.override_doctor = overrideDoctor; - exception.override_title = payload.overrideTitle || null; - exception.override_start_at = payload.overrideStartAt ? new Date(payload.overrideStartAt) : null; - exception.override_end_at = payload.overrideEndAt ? new Date(payload.overrideEndAt) : null; - exception.updated_by = actor; + exception.is_cancelled = !!payload.isCancelled; + exception.override_room_id = payload.overrideRoomId || null; + exception.override_doctor_id = payload.overrideDoctorId || null; + exception.override_room = overrideRoom; + exception.override_doctor = overrideDoctor; + exception.override_title = payload.overrideTitle || null; + exception.override_start_at = payload.overrideStartAt ? new Date(payload.overrideStartAt) : null; + exception.override_end_at = payload.overrideEndAt ? new Date(payload.overrideEndAt) : null; + exception.updated_by = actor; - return OrmHelper.DB.manager.save(exception); - } + return OrmHelper.DB.manager.save(exception); + } - private static expandSeriesOccurrences(series: ScheduleSeries, rangeStart: Date, rangeEnd: Date): Date[] { - const seriesStart = ScheduleModel.startOfDay(new Date(series.start_date)); - const seriesUntil = series.until_date ? ScheduleModel.endOfDay(new Date(series.until_date)) : null; - const rule = ScheduleModel.parseRRule(series.recurrence_rule || ""); - const ruleUntil = rule.until ? ScheduleModel.endOfDay(rule.until) : null; + private static expandSeriesOccurrences(series: ScheduleSeries, rangeStart: Date, rangeEnd: Date): Date[] { + const seriesStart = ScheduleModel.startOfDay(new Date(series.start_date)); + const seriesUntil = series.until_date ? ScheduleModel.endOfDay(new Date(series.until_date)) : null; + const rule = ScheduleModel.parseRRule(series.recurrence_rule || ""); + const ruleUntil = rule.until ? ScheduleModel.endOfDay(rule.until) : null; - const effectiveStart = new Date(Math.max(rangeStart.getTime(), seriesStart.getTime())); - let effectiveEnd = new Date(rangeEnd); - if (seriesUntil && seriesUntil < effectiveEnd) effectiveEnd = seriesUntil; - if (ruleUntil && ruleUntil < effectiveEnd) effectiveEnd = ruleUntil; - if (effectiveStart > effectiveEnd) return []; + const effectiveStart = new Date(Math.max(rangeStart.getTime(), seriesStart.getTime())); + let effectiveEnd = new Date(rangeEnd); + if (seriesUntil && seriesUntil < effectiveEnd) effectiveEnd = seriesUntil; + if (ruleUntil && ruleUntil < effectiveEnd) effectiveEnd = ruleUntil; + if (effectiveStart > effectiveEnd) return []; - if (!rule.freq) { - return seriesStart >= effectiveStart && seriesStart <= effectiveEnd ? [seriesStart] : []; - } + if (!rule.freq) { + return seriesStart >= effectiveStart && seriesStart <= effectiveEnd ? [seriesStart] : []; + } - const dates: Date[] = []; - let generatedCount = 0; - let current = new Date(seriesStart); - const byDay = rule.byDay?.length ? rule.byDay : [ScheduleModel.weekdayToRRule(seriesStart.getDay())]; - const interval = rule.interval || 1; + const dates: Date[] = []; + let generatedCount = 0; + let current = new Date(seriesStart); + const byDay = rule.byDay?.length ? rule.byDay : [ScheduleModel.weekdayToRRule(seriesStart.getDay())]; + const interval = rule.interval || 1; - while (current <= effectiveEnd) { - if (ScheduleModel.matchesRule(current, seriesStart, rule.freq, interval, byDay)) { - generatedCount += 1; - if (current >= effectiveStart) { - dates.push(new Date(current)); - } - if (rule.count && generatedCount >= rule.count) break; - } - current.setDate(current.getDate() + 1); - } + while (current <= effectiveEnd) { + if (ScheduleModel.matchesRule(current, seriesStart, rule.freq, interval, byDay)) { + generatedCount += 1; + if (current >= effectiveStart) { + dates.push(new Date(current)); + } + if (rule.count && generatedCount >= rule.count) break; + } + current.setDate(current.getDate() + 1); + } - return dates; - } + return dates; + } - private static matchesRule(date: Date, start: Date, freq: string, interval: number, byDay: string[]): boolean { - const diffDays = Math.floor((ScheduleModel.startOfDay(date).getTime() - ScheduleModel.startOfDay(start).getTime()) / 86400000); - if (diffDays < 0) return false; + private static matchesRule(date: Date, start: Date, freq: string, interval: number, byDay: string[]): boolean { + const diffDays = Math.floor((ScheduleModel.startOfDay(date).getTime() - ScheduleModel.startOfDay(start).getTime()) / 86400000); + if (diffDays < 0) return false; - if (freq === "DAILY") return diffDays % interval === 0; - if (freq === "WEEKLY") { - const weekIndex = Math.floor(diffDays / 7); - return weekIndex % interval === 0 && byDay.includes(ScheduleModel.weekdayToRRule(date.getDay())); - } - if (freq === "MONTHLY") { - const months = (date.getFullYear() - start.getFullYear()) * 12 + (date.getMonth() - start.getMonth()); - return months % interval === 0 && date.getDate() === start.getDate(); - } - return false; - } + if (freq === "DAILY") return diffDays % interval === 0; + if (freq === "WEEKLY") { + const weekIndex = Math.floor(diffDays / 7); + return weekIndex % interval === 0 && byDay.includes(ScheduleModel.weekdayToRRule(date.getDay())); + } + if (freq === "MONTHLY") { + const months = (date.getFullYear() - start.getFullYear()) * 12 + (date.getMonth() - start.getMonth()); + return months % interval === 0 && date.getDate() === start.getDate(); + } + return false; + } - private static parseRRule(rule: string): { freq?: string; interval?: number; count?: number; until?: Date; byDay?: string[] } { - if (!rule) return {}; - const pairs = rule.split(";").map((x) => x.trim()).filter(Boolean); - const out: { [key: string]: string } = {}; - for (const p of pairs) { - const [k, v] = p.split("="); - if (k && v) out[k.toUpperCase()] = v; - } + private static parseRRule(rule: string): { freq?: string; interval?: number; count?: number; until?: Date; byDay?: string[] } { + if (!rule) return {}; + const pairs = rule.split(";").map((x) => x.trim()).filter(Boolean); + const out: { [key: string]: string } = {}; + for (const p of pairs) { + const [k, v] = p.split("="); + if (k && v) out[k.toUpperCase()] = v; + } - const untilRaw = out.UNTIL; - let untilDate: Date = null; - if (untilRaw) { - if (/^\d{8}$/.test(untilRaw)) { - untilDate = new Date(`${untilRaw.slice(0, 4)}-${untilRaw.slice(4, 6)}-${untilRaw.slice(6, 8)}T00:00:00`); - } else if (/^\d{8}T\d{6}Z$/.test(untilRaw)) { - untilDate = new Date(`${untilRaw.slice(0, 4)}-${untilRaw.slice(4, 6)}-${untilRaw.slice(6, 8)}T${untilRaw.slice(9, 11)}:${untilRaw.slice(11, 13)}:${untilRaw.slice(13, 15)}Z`); - } else { - untilDate = new Date(untilRaw); - } - } + const untilRaw = out.UNTIL; + let untilDate: Date = null; + if (untilRaw) { + if (/^\d{8}$/.test(untilRaw)) { + untilDate = new Date(`${untilRaw.slice(0, 4)}-${untilRaw.slice(4, 6)}-${untilRaw.slice(6, 8)}T00:00:00`); + } else if (/^\d{8}T\d{6}Z$/.test(untilRaw)) { + untilDate = new Date(`${untilRaw.slice(0, 4)}-${untilRaw.slice(4, 6)}-${untilRaw.slice(6, 8)}T${untilRaw.slice(9, 11)}:${untilRaw.slice(11, 13)}:${untilRaw.slice(13, 15)}Z`); + } else { + untilDate = new Date(untilRaw); + } + } - return { - freq: out.FREQ?.toUpperCase(), - interval: out.INTERVAL ? Number(out.INTERVAL) : 1, - count: out.COUNT ? Number(out.COUNT) : undefined, - until: untilDate && !isNaN(untilDate.getTime()) ? untilDate : undefined, - byDay: out.BYDAY ? out.BYDAY.split(",").map((x) => x.trim().toUpperCase()) : undefined, - }; - } + return { + freq: out.FREQ?.toUpperCase(), + interval: out.INTERVAL ? Number(out.INTERVAL) : 1, + count: out.COUNT ? Number(out.COUNT) : undefined, + until: untilDate && !isNaN(untilDate.getTime()) ? untilDate : undefined, + byDay: out.BYDAY ? out.BYDAY.split(",").map((x) => x.trim().toUpperCase()) : undefined, + }; + } - private static combineDateAndTime(ymd: string, hhmmss: string): Date { - return new Date(`${ymd}T${hhmmss}`); - } + private static combineDateAndTime(ymd: string, hhmmss: string): Date { + return new Date(`${ymd}T${hhmmss}`); + } - private static startOfDay(date: Date): Date { - return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0); - } + private static startOfDay(date: Date): Date { + return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0); + } - private static endOfDay(date: Date): Date { - return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999); - } + private static endOfDay(date: Date): Date { + return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999); + } - private static formatYmd(date: Date): string { - const y = date.getFullYear(); - const m = String(date.getMonth() + 1).padStart(2, "0"); - const d = String(date.getDate()).padStart(2, "0"); - return `${y}-${m}-${d}`; - } + private static formatYmd(date: Date): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, "0"); + const d = String(date.getDate()).padStart(2, "0"); + return `${y}-${m}-${d}`; + } - private static weekdayToRRule(day: number): string { - return ["SU", "MO", "TU", "WE", "TH", "FR", "SA"][day]; - } + private static weekdayToRRule(day: number): string { + return ["SU", "MO", "TU", "WE", "TH", "FR", "SA"][day]; + } - private static recurrenceToRRule(recurrence: RecurrenceInput | null): string { - if (!recurrence) return null; - const interval = recurrence.interval && recurrence.interval > 0 ? recurrence.interval : 1; - if (recurrence.type === "weekly") { - const byDay = recurrence.daysOfWeek && recurrence.daysOfWeek.length > 0 ? recurrence.daysOfWeek.join(",") : ""; - return byDay ? `FREQ=WEEKLY;INTERVAL=${interval};BYDAY=${byDay}` : `FREQ=WEEKLY;INTERVAL=${interval}`; - } - if (recurrence.type === "daily") { - return `FREQ=DAILY;INTERVAL=${interval}`; - } - if (recurrence.type === "monthly") { - return `FREQ=MONTHLY;INTERVAL=${interval}`; - } - throw new Error("Unsupported recurrence type"); - } + private static recurrenceToRRule(recurrence: RecurrenceInput | null): string { + if (!recurrence) return null; + const interval = recurrence.interval && recurrence.interval > 0 ? recurrence.interval : 1; + if (recurrence.type === "weekly") { + const byDay = recurrence.daysOfWeek && recurrence.daysOfWeek.length > 0 ? recurrence.daysOfWeek.join(",") : ""; + return byDay ? `FREQ=WEEKLY;INTERVAL=${interval};BYDAY=${byDay}` : `FREQ=WEEKLY;INTERVAL=${interval}`; + } + if (recurrence.type === "daily") { + return `FREQ=DAILY;INTERVAL=${interval}`; + } + if (recurrence.type === "monthly") { + return `FREQ=MONTHLY;INTERVAL=${interval}`; + } + throw new Error("Unsupported recurrence type"); + } }