fixing timezone
This commit is contained in:
@ -4,455 +4,455 @@ import CommonHelper from "../helpers/common";
|
|||||||
import { OrmHelper } from "../helpers/orm";
|
import { OrmHelper } from "../helpers/orm";
|
||||||
|
|
||||||
type RecurrenceInput = {
|
type RecurrenceInput = {
|
||||||
type: "daily" | "weekly" | "monthly";
|
type: "daily" | "weekly" | "monthly";
|
||||||
interval?: number;
|
interval?: number;
|
||||||
daysOfWeek?: string[];
|
daysOfWeek?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ScheduleSeriesPayload = {
|
type ScheduleSeriesPayload = {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
doctorId: string;
|
doctorId: string;
|
||||||
title: string;
|
title: string;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
untilDate?: string | null;
|
untilDate?: string | null;
|
||||||
startTimeLocal: string;
|
startTimeLocal: string;
|
||||||
endTimeLocal: string;
|
endTimeLocal: string;
|
||||||
recurrence?: RecurrenceInput | null;
|
recurrence?: RecurrenceInput | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ScheduleExceptionPayload = {
|
type ScheduleExceptionPayload = {
|
||||||
occurrenceDate: string;
|
occurrenceDate: string;
|
||||||
isCancelled?: boolean;
|
isCancelled?: boolean;
|
||||||
overrideRoomId?: string | null;
|
overrideRoomId?: string | null;
|
||||||
overrideDoctorId?: string | null;
|
overrideDoctorId?: string | null;
|
||||||
overrideTitle?: string | null;
|
overrideTitle?: string | null;
|
||||||
overrideStartAt?: string | null;
|
overrideStartAt?: string | null;
|
||||||
overrideEndAt?: string | null;
|
overrideEndAt?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CalendarQueryOptions = {
|
type CalendarQueryOptions = {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
page?: number;
|
page?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ScheduleModel {
|
export class ScheduleModel {
|
||||||
static async listSeries(filterObj = {}): Promise<SelectQueryBuilder<any>> {
|
static async listSeries(filterObj = {}): Promise<SelectQueryBuilder<any>> {
|
||||||
const repo = OrmHelper.DB.getRepository(ScheduleSeries);
|
const repo = OrmHelper.DB.getRepository(ScheduleSeries);
|
||||||
let whereAttr: string[] = [];
|
let whereAttr: string[] = [];
|
||||||
let whereVal: any = {};
|
let whereVal: any = {};
|
||||||
if (filterObj && Object.keys(filterObj).length > 0) {
|
if (filterObj && Object.keys(filterObj).length > 0) {
|
||||||
const filterResult = CommonHelper.handleQueryFilter(filterObj);
|
const filterResult = CommonHelper.handleQueryFilter(filterObj);
|
||||||
whereAttr = [...whereAttr, ...filterResult.whereAttr];
|
whereAttr = [...whereAttr, ...filterResult.whereAttr];
|
||||||
whereVal = { ...whereVal, ...filterResult.whereVal };
|
whereVal = { ...whereVal, ...filterResult.whereVal };
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = repo
|
let query = repo
|
||||||
.createQueryBuilder("ScheduleSeries")
|
.createQueryBuilder("ScheduleSeries")
|
||||||
.leftJoinAndSelect("ScheduleSeries.doctor", "Doctor")
|
.leftJoinAndSelect("ScheduleSeries.doctor", "Doctor")
|
||||||
.leftJoinAndSelect("ScheduleSeries.room", "Room");
|
.leftJoinAndSelect("ScheduleSeries.room", "Room");
|
||||||
if (whereAttr.length !== 0) {
|
if (whereAttr.length !== 0) {
|
||||||
query = query.where(whereAttr.join(" and "), whereVal);
|
query = query.where(whereAttr.join(" and "), whereVal);
|
||||||
}
|
}
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getScheduleCalendarRange(start: string, end: string, filterObj = {}, options: CalendarQueryOptions = {}): Promise<any> {
|
static async getScheduleCalendarRange(start: string, end: string, filterObj = {}, options: CalendarQueryOptions = {}): Promise<any> {
|
||||||
const startDate = new Date(start);
|
const startDate = new Date(start);
|
||||||
const endDate = new Date(end);
|
const endDate = new Date(end);
|
||||||
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
||||||
throw new Error("Invalid start or end date");
|
throw new Error("Invalid start or end date");
|
||||||
}
|
}
|
||||||
|
|
||||||
const rangeStart = ScheduleModel.startOfDay(startDate);
|
const rangeStart = ScheduleModel.startOfDay(startDate);
|
||||||
const rangeEnd = ScheduleModel.endOfDay(endDate);
|
const rangeEnd = ScheduleModel.endOfDay(endDate);
|
||||||
if (rangeStart > rangeEnd) {
|
if (rangeStart > rangeEnd) {
|
||||||
throw new Error("Start date must be before end date");
|
throw new Error("Start date must be before end date");
|
||||||
}
|
}
|
||||||
|
|
||||||
const seriesQuery = await ScheduleModel.listSeries(filterObj);
|
const seriesQuery = await ScheduleModel.listSeries(filterObj);
|
||||||
seriesQuery
|
seriesQuery
|
||||||
.andWhere("ScheduleSeries.start_date <= :rangeEndDate", { rangeEndDate: ScheduleModel.formatYmd(rangeEnd) })
|
.andWhere("ScheduleSeries.start_date <= :rangeEndDate", { rangeEndDate: ScheduleModel.formatYmd(rangeEnd) })
|
||||||
.andWhere("(ScheduleSeries.until_date IS NULL OR ScheduleSeries.until_date >= :rangeStartDate)", {
|
.andWhere("(ScheduleSeries.until_date IS NULL OR ScheduleSeries.until_date >= :rangeStartDate)", {
|
||||||
rangeStartDate: ScheduleModel.formatYmd(rangeStart),
|
rangeStartDate: ScheduleModel.formatYmd(rangeStart),
|
||||||
})
|
})
|
||||||
.andWhere("ScheduleSeries.deleted_at IS NULL");
|
.andWhere("ScheduleSeries.deleted_at IS NULL");
|
||||||
|
|
||||||
const seriesList: ScheduleSeries[] = await seriesQuery.getMany();
|
const seriesList: ScheduleSeries[] = await seriesQuery.getMany();
|
||||||
if (seriesList.length === 0) return { resources: [], events: [] };
|
if (seriesList.length === 0) return { resources: [], events: [] };
|
||||||
|
|
||||||
const seriesIds = seriesList.map((series) => series.id);
|
const seriesIds = seriesList.map((series) => series.id);
|
||||||
const exceptionRepo = OrmHelper.DB.getRepository(ScheduleException);
|
const exceptionRepo = OrmHelper.DB.getRepository(ScheduleException);
|
||||||
const exceptions = await exceptionRepo
|
const exceptions = await exceptionRepo
|
||||||
.createQueryBuilder("ScheduleException")
|
.createQueryBuilder("ScheduleException")
|
||||||
.leftJoinAndSelect("ScheduleException.override_room", "OverrideRoom")
|
.leftJoinAndSelect("ScheduleException.override_room", "OverrideRoom")
|
||||||
.leftJoinAndSelect("ScheduleException.override_doctor", "OverrideDoctor")
|
.leftJoinAndSelect("ScheduleException.override_doctor", "OverrideDoctor")
|
||||||
.where("ScheduleException.series_id IN (:...seriesIds)", { seriesIds })
|
.where("ScheduleException.series_id IN (:...seriesIds)", { seriesIds })
|
||||||
.andWhere("ScheduleException.occurrence_date BETWEEN :rangeStart AND :rangeEnd", {
|
.andWhere("ScheduleException.occurrence_date BETWEEN :rangeStart AND :rangeEnd", {
|
||||||
rangeStart: ScheduleModel.formatYmd(rangeStart),
|
rangeStart: ScheduleModel.formatYmd(rangeStart),
|
||||||
rangeEnd: ScheduleModel.formatYmd(rangeEnd),
|
rangeEnd: ScheduleModel.formatYmd(rangeEnd),
|
||||||
})
|
})
|
||||||
.andWhere("ScheduleException.deleted_at IS NULL")
|
.andWhere("ScheduleException.deleted_at IS NULL")
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
const exceptionMap = new Map<string, ScheduleException>();
|
const exceptionMap = new Map<string, ScheduleException>();
|
||||||
for (const ex of exceptions) {
|
for (const ex of exceptions) {
|
||||||
const key = `${ex.series_id}:${ScheduleModel.formatYmd(new Date(ex.occurrence_date))}`;
|
const key = `${ex.series_id}:${ScheduleModel.formatYmd(new Date(ex.occurrence_date))}`;
|
||||||
exceptionMap.set(key, ex);
|
exceptionMap.set(key, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allEvents: any[] = [];
|
const allEvents: any[] = [];
|
||||||
|
|
||||||
for (const series of seriesList) {
|
for (const series of seriesList) {
|
||||||
const occurrenceDates = ScheduleModel.expandSeriesOccurrences(series, rangeStart, rangeEnd);
|
const occurrenceDates = ScheduleModel.expandSeriesOccurrences(series, rangeStart, rangeEnd);
|
||||||
for (const occurrenceDate of occurrenceDates) {
|
for (const occurrenceDate of occurrenceDates) {
|
||||||
const occurrenceYmd = ScheduleModel.formatYmd(occurrenceDate);
|
const occurrenceYmd = ScheduleModel.formatYmd(occurrenceDate);
|
||||||
const exception = exceptionMap.get(`${series.id}:${occurrenceYmd}`);
|
const exception = exceptionMap.get(`${series.id}:${occurrenceYmd}`);
|
||||||
if (exception && exception.is_cancelled) continue;
|
if (exception && exception.is_cancelled) continue;
|
||||||
|
|
||||||
const defaultStart = ScheduleModel.combineDateAndTime(occurrenceYmd, series.start_time_local);
|
const defaultStart = ScheduleModel.combineDateAndTime(occurrenceYmd, series.start_time_local);
|
||||||
const defaultEnd = ScheduleModel.combineDateAndTime(occurrenceYmd, series.end_time_local);
|
const defaultEnd = ScheduleModel.combineDateAndTime(occurrenceYmd, series.end_time_local);
|
||||||
|
|
||||||
const doctor = exception?.override_doctor ?? series.doctor;
|
const doctor = exception?.override_doctor ?? series.doctor;
|
||||||
const room = exception?.override_room ?? series.room;
|
const room = exception?.override_room ?? series.room;
|
||||||
const eventStart = exception?.override_start_at ? new Date(exception.override_start_at) : defaultStart;
|
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 eventEnd = exception?.override_end_at ? new Date(exception.override_end_at) : defaultEnd;
|
||||||
const eventTitle = exception?.override_title || series.title;
|
const eventTitle = exception?.override_title || series.title;
|
||||||
|
|
||||||
const roomId = room?.id || null;
|
const roomId = room?.id || null;
|
||||||
const doctorId = doctor?.id || null;
|
const doctorId = doctor?.id || null;
|
||||||
const resourceId = `${roomId || "unknown-room"}:${doctorId || "unknown-doctor"}`;
|
const resourceId = `${roomId || "unknown-room"}:${doctorId || "unknown-doctor"}`;
|
||||||
const roomName = (room as any)?.room || (room as any)?.name || (room as any)?.code || "";
|
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 doctorName = (doctor as any)?.name || (doctor as any)?.fullname || (doctor as any)?.username || "";
|
||||||
|
|
||||||
allEvents.push({
|
allEvents.push({
|
||||||
id: `occ:${series.id}:${occurrenceYmd}`,
|
id: `occ:${series.id}:${occurrenceYmd}`,
|
||||||
seriesId: series.id,
|
seriesId: series.id,
|
||||||
resourceId,
|
resourceId,
|
||||||
title: eventTitle,
|
title: eventTitle,
|
||||||
startAt: eventStart.toISOString(),
|
startAt: eventStart.toLocaleString('en-US', { timeZone: series.timezone }),
|
||||||
endAt: eventEnd.toISOString(),
|
endAt: eventEnd.toLocaleString('en-US', { timeZone: series.timezone }),
|
||||||
isRecurring: !!series.recurrence_rule,
|
isRecurring: !!series.recurrence_rule,
|
||||||
isException: !!exception,
|
isException: !!exception,
|
||||||
status: "confirmed",
|
status: "confirmed",
|
||||||
_resource: {
|
_resource: {
|
||||||
id: resourceId,
|
id: resourceId,
|
||||||
roomId,
|
roomId,
|
||||||
roomName,
|
roomName,
|
||||||
doctorId,
|
doctorId,
|
||||||
doctorName,
|
doctorName,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allResourcesMap = new Map<string, any>();
|
const allResourcesMap = new Map<string, any>();
|
||||||
for (const event of allEvents) {
|
for (const event of allEvents) {
|
||||||
allResourcesMap.set(event._resource.id, event._resource);
|
allResourcesMap.set(event._resource.id, event._resource);
|
||||||
}
|
}
|
||||||
const allResources = [...allResourcesMap.values()].sort((a, b) => a.id.localeCompare(b.id));
|
const allResources = [...allResourcesMap.values()].sort((a, b) => a.id.localeCompare(b.id));
|
||||||
const totalCount = allResources.length;
|
const totalCount = allResources.length;
|
||||||
const page = options.page ?? 1;
|
const page = options.page ?? 1;
|
||||||
const limit = options.limit ?? totalCount;
|
const limit = options.limit ?? totalCount;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
const pagedResources = allResources.slice(offset, offset + limit);
|
const pagedResources = allResources.slice(offset, offset + limit);
|
||||||
const allowedResourceIds = new Set(pagedResources.map((resource) => resource.id));
|
const allowedResourceIds = new Set(pagedResources.map((resource) => resource.id));
|
||||||
|
|
||||||
const pagedEvents = allEvents
|
const pagedEvents = allEvents
|
||||||
.filter((event) => allowedResourceIds.has(event.resourceId))
|
.filter((event) => allowedResourceIds.has(event.resourceId))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a.startAt === b.startAt) return a.id.localeCompare(b.id);
|
if (a.startAt === b.startAt) return a.id.localeCompare(b.id);
|
||||||
return a.startAt.localeCompare(b.startAt);
|
return a.startAt.localeCompare(b.startAt);
|
||||||
})
|
})
|
||||||
.map((event) => {
|
.map((event) => {
|
||||||
const { _resource, ...cleanEvent } = event;
|
const { _resource, ...cleanEvent } = event;
|
||||||
return cleanEvent;
|
return cleanEvent;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
resources: pagedResources,
|
resources: pagedResources,
|
||||||
events: pagedEvents,
|
events: pagedEvents,
|
||||||
meta: {
|
meta: {
|
||||||
total_count: totalCount,
|
total_count: totalCount,
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getScheduleOptions(filterObj = {}): Promise<any> {
|
static async getScheduleOptions(filterObj = {}): Promise<any> {
|
||||||
const query = await ScheduleModel.listSeries(filterObj);
|
const query = await ScheduleModel.listSeries(filterObj);
|
||||||
query.andWhere("ScheduleSeries.deleted_at IS NULL");
|
query.andWhere("ScheduleSeries.deleted_at IS NULL");
|
||||||
const seriesList: ScheduleSeries[] = await query.getMany();
|
const seriesList: ScheduleSeries[] = await query.getMany();
|
||||||
|
|
||||||
const doctorsMap = new Map<string, any>();
|
const doctorsMap = new Map<string, any>();
|
||||||
const roomsMap = new Map<string, any>();
|
const roomsMap = new Map<string, any>();
|
||||||
|
|
||||||
for (const series of seriesList) {
|
for (const series of seriesList) {
|
||||||
if (series.doctor?.id) {
|
if (series.doctor?.id) {
|
||||||
doctorsMap.set(series.doctor.id, {
|
doctorsMap.set(series.doctor.id, {
|
||||||
id: series.doctor.id,
|
id: series.doctor.id,
|
||||||
title: (series.doctor as any).name || (series.doctor as any).username || "Doctor",
|
title: (series.doctor as any).name || (series.doctor as any).username || "Doctor",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (series.room?.id) {
|
if (series.room?.id) {
|
||||||
roomsMap.set(series.room.id, {
|
roomsMap.set(series.room.id, {
|
||||||
id: series.room.id,
|
id: series.room.id,
|
||||||
title: (series.room as any).room || (series.room as any).code || "Room",
|
title: (series.room as any).room || (series.room as any).code || "Room",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
doctors: [...doctorsMap.values()],
|
doctors: [...doctorsMap.values()],
|
||||||
rooms: [...roomsMap.values()],
|
rooms: [...roomsMap.values()],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getSeriesById(id: string): Promise<any> {
|
static async getSeriesById(id: string): Promise<any> {
|
||||||
const series = await OrmHelper.DB.getRepository(ScheduleSeries)
|
const series = await OrmHelper.DB.getRepository(ScheduleSeries)
|
||||||
.createQueryBuilder("ScheduleSeries")
|
.createQueryBuilder("ScheduleSeries")
|
||||||
.leftJoinAndSelect("ScheduleSeries.doctor", "Doctor")
|
.leftJoinAndSelect("ScheduleSeries.doctor", "Doctor")
|
||||||
.leftJoinAndSelect("ScheduleSeries.room", "Room")
|
.leftJoinAndSelect("ScheduleSeries.room", "Room")
|
||||||
.where("ScheduleSeries.id = :id", { id })
|
.where("ScheduleSeries.id = :id", { id })
|
||||||
.andWhere("ScheduleSeries.deleted_at IS NULL")
|
.andWhere("ScheduleSeries.deleted_at IS NULL")
|
||||||
.getOne();
|
.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)
|
const exceptions = await OrmHelper.DB.getRepository(ScheduleException)
|
||||||
.createQueryBuilder("ScheduleException")
|
.createQueryBuilder("ScheduleException")
|
||||||
.leftJoinAndSelect("ScheduleException.override_room", "OverrideRoom")
|
.leftJoinAndSelect("ScheduleException.override_room", "OverrideRoom")
|
||||||
.leftJoinAndSelect("ScheduleException.override_doctor", "OverrideDoctor")
|
.leftJoinAndSelect("ScheduleException.override_doctor", "OverrideDoctor")
|
||||||
.where("ScheduleException.series_id = :id", { id })
|
.where("ScheduleException.series_id = :id", { id })
|
||||||
.andWhere("ScheduleException.deleted_at IS NULL")
|
.andWhere("ScheduleException.deleted_at IS NULL")
|
||||||
.orderBy("ScheduleException.occurrence_date", "ASC")
|
.orderBy("ScheduleException.occurrence_date", "ASC")
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
return { series, exceptions };
|
return { series, exceptions };
|
||||||
}
|
}
|
||||||
|
|
||||||
static async createSeries(payload: ScheduleSeriesPayload, actor: string): Promise<ScheduleSeries> {
|
static async createSeries(payload: ScheduleSeriesPayload, actor: string): Promise<ScheduleSeries> {
|
||||||
const room = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.roomId } });
|
const room = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.roomId } });
|
||||||
if (!room) throw new Error("Room not found");
|
if (!room) throw new Error("Room not found");
|
||||||
|
|
||||||
const doctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.doctorId } });
|
const doctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.doctorId } });
|
||||||
if (!doctor) throw new Error("Doctor not found");
|
if (!doctor) throw new Error("Doctor not found");
|
||||||
|
|
||||||
const series = new ScheduleSeries();
|
const series = new ScheduleSeries();
|
||||||
series.room_id = payload.roomId;
|
series.room_id = payload.roomId;
|
||||||
series.doctor_id = payload.doctorId;
|
series.doctor_id = payload.doctorId;
|
||||||
series.room = room;
|
series.room = room;
|
||||||
series.doctor = doctor;
|
series.doctor = doctor;
|
||||||
series.title = payload.title;
|
series.title = payload.title;
|
||||||
series.timezone = payload.timezone;
|
series.timezone = payload.timezone;
|
||||||
series.start_date = new Date(payload.startDate);
|
series.start_date = new Date(payload.startDate);
|
||||||
series.until_date = payload.untilDate ? new Date(payload.untilDate) : null;
|
series.until_date = payload.untilDate ? new Date(payload.untilDate) : null;
|
||||||
series.start_time_local = payload.startTimeLocal;
|
series.start_time_local = payload.startTimeLocal;
|
||||||
series.end_time_local = payload.endTimeLocal;
|
series.end_time_local = payload.endTimeLocal;
|
||||||
series.recurrence_rule = ScheduleModel.recurrenceToRRule(payload.recurrence || null);
|
series.recurrence_rule = ScheduleModel.recurrenceToRRule(payload.recurrence || null);
|
||||||
series.status = Status.Active;
|
series.status = Status.Active;
|
||||||
series.created_by = actor;
|
series.created_by = actor;
|
||||||
series.updated_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<ScheduleSeries> {
|
static async updateSeries(id: string, payload: ScheduleSeriesPayload, actor: string): Promise<ScheduleSeries> {
|
||||||
const series = await OrmHelper.DB.getRepository(ScheduleSeries)
|
const series = await OrmHelper.DB.getRepository(ScheduleSeries)
|
||||||
.createQueryBuilder("ScheduleSeries")
|
.createQueryBuilder("ScheduleSeries")
|
||||||
.where("ScheduleSeries.id = :id", { id })
|
.where("ScheduleSeries.id = :id", { id })
|
||||||
.andWhere("ScheduleSeries.deleted_at IS NULL")
|
.andWhere("ScheduleSeries.deleted_at IS NULL")
|
||||||
.getOne();
|
.getOne();
|
||||||
if (!series) throw new Error("Schedule series not found");
|
if (!series) throw new Error("Schedule series not found");
|
||||||
|
|
||||||
const room = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.roomId } });
|
const room = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.roomId } });
|
||||||
if (!room) throw new Error("Room not found");
|
if (!room) throw new Error("Room not found");
|
||||||
|
|
||||||
const doctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.doctorId } });
|
const doctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.doctorId } });
|
||||||
if (!doctor) throw new Error("Doctor not found");
|
if (!doctor) throw new Error("Doctor not found");
|
||||||
|
|
||||||
series.room_id = payload.roomId;
|
series.room_id = payload.roomId;
|
||||||
series.doctor_id = payload.doctorId;
|
series.doctor_id = payload.doctorId;
|
||||||
series.room = room;
|
series.room = room;
|
||||||
series.doctor = doctor;
|
series.doctor = doctor;
|
||||||
series.title = payload.title;
|
series.title = payload.title;
|
||||||
series.timezone = payload.timezone;
|
series.timezone = payload.timezone;
|
||||||
series.start_date = new Date(payload.startDate);
|
series.start_date = new Date(payload.startDate);
|
||||||
series.until_date = payload.untilDate ? new Date(payload.untilDate) : null;
|
series.until_date = payload.untilDate ? new Date(payload.untilDate) : null;
|
||||||
series.start_time_local = payload.startTimeLocal;
|
series.start_time_local = payload.startTimeLocal;
|
||||||
series.end_time_local = payload.endTimeLocal;
|
series.end_time_local = payload.endTimeLocal;
|
||||||
series.recurrence_rule = ScheduleModel.recurrenceToRRule(payload.recurrence || null);
|
series.recurrence_rule = ScheduleModel.recurrenceToRRule(payload.recurrence || null);
|
||||||
series.updated_by = actor;
|
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<ScheduleException> {
|
static async createSeriesException(seriesId: string, payload: ScheduleExceptionPayload, actor: string): Promise<ScheduleException> {
|
||||||
const series = await OrmHelper.DB.getRepository(ScheduleSeries).findOne({ where: { id: seriesId } });
|
const series = await OrmHelper.DB.getRepository(ScheduleSeries).findOne({ where: { id: seriesId } });
|
||||||
if (!series) throw new Error("Schedule series not found");
|
if (!series) throw new Error("Schedule series not found");
|
||||||
|
|
||||||
let overrideRoom: Room = null;
|
let overrideRoom: Room = null;
|
||||||
if (payload.overrideRoomId) {
|
if (payload.overrideRoomId) {
|
||||||
overrideRoom = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.overrideRoomId } });
|
overrideRoom = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.overrideRoomId } });
|
||||||
if (!overrideRoom) throw new Error("Override room not found");
|
if (!overrideRoom) throw new Error("Override room not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
let overrideDoctor: User = null;
|
let overrideDoctor: User = null;
|
||||||
if (payload.overrideDoctorId) {
|
if (payload.overrideDoctorId) {
|
||||||
overrideDoctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.overrideDoctorId } });
|
overrideDoctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.overrideDoctorId } });
|
||||||
if (!overrideDoctor) throw new Error("Override doctor not found");
|
if (!overrideDoctor) throw new Error("Override doctor not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const occurrenceDate = new Date(payload.occurrenceDate);
|
const occurrenceDate = new Date(payload.occurrenceDate);
|
||||||
const occurrenceYmd = ScheduleModel.formatYmd(occurrenceDate);
|
const occurrenceYmd = ScheduleModel.formatYmd(occurrenceDate);
|
||||||
|
|
||||||
let exception = await OrmHelper.DB.getRepository(ScheduleException)
|
let exception = await OrmHelper.DB.getRepository(ScheduleException)
|
||||||
.createQueryBuilder("ScheduleException")
|
.createQueryBuilder("ScheduleException")
|
||||||
.where("ScheduleException.series_id = :seriesId", { seriesId })
|
.where("ScheduleException.series_id = :seriesId", { seriesId })
|
||||||
.andWhere("ScheduleException.occurrence_date = :occurrenceDate", { occurrenceDate: occurrenceYmd })
|
.andWhere("ScheduleException.occurrence_date = :occurrenceDate", { occurrenceDate: occurrenceYmd })
|
||||||
.andWhere("ScheduleException.deleted_at IS NULL")
|
.andWhere("ScheduleException.deleted_at IS NULL")
|
||||||
.getOne();
|
.getOne();
|
||||||
|
|
||||||
if (!exception) {
|
if (!exception) {
|
||||||
exception = new ScheduleException();
|
exception = new ScheduleException();
|
||||||
exception.series_id = seriesId;
|
exception.series_id = seriesId;
|
||||||
exception.series = series;
|
exception.series = series;
|
||||||
exception.occurrence_date = occurrenceDate;
|
exception.occurrence_date = occurrenceDate;
|
||||||
exception.created_by = actor;
|
exception.created_by = actor;
|
||||||
}
|
}
|
||||||
|
|
||||||
exception.is_cancelled = !!payload.isCancelled;
|
exception.is_cancelled = !!payload.isCancelled;
|
||||||
exception.override_room_id = payload.overrideRoomId || null;
|
exception.override_room_id = payload.overrideRoomId || null;
|
||||||
exception.override_doctor_id = payload.overrideDoctorId || null;
|
exception.override_doctor_id = payload.overrideDoctorId || null;
|
||||||
exception.override_room = overrideRoom;
|
exception.override_room = overrideRoom;
|
||||||
exception.override_doctor = overrideDoctor;
|
exception.override_doctor = overrideDoctor;
|
||||||
exception.override_title = payload.overrideTitle || null;
|
exception.override_title = payload.overrideTitle || null;
|
||||||
exception.override_start_at = payload.overrideStartAt ? new Date(payload.overrideStartAt) : null;
|
exception.override_start_at = payload.overrideStartAt ? new Date(payload.overrideStartAt) : null;
|
||||||
exception.override_end_at = payload.overrideEndAt ? new Date(payload.overrideEndAt) : null;
|
exception.override_end_at = payload.overrideEndAt ? new Date(payload.overrideEndAt) : null;
|
||||||
exception.updated_by = actor;
|
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[] {
|
private static expandSeriesOccurrences(series: ScheduleSeries, rangeStart: Date, rangeEnd: Date): Date[] {
|
||||||
const seriesStart = ScheduleModel.startOfDay(new Date(series.start_date));
|
const seriesStart = ScheduleModel.startOfDay(new Date(series.start_date));
|
||||||
const seriesUntil = series.until_date ? ScheduleModel.endOfDay(new Date(series.until_date)) : null;
|
const seriesUntil = series.until_date ? ScheduleModel.endOfDay(new Date(series.until_date)) : null;
|
||||||
const rule = ScheduleModel.parseRRule(series.recurrence_rule || "");
|
const rule = ScheduleModel.parseRRule(series.recurrence_rule || "");
|
||||||
const ruleUntil = rule.until ? ScheduleModel.endOfDay(rule.until) : null;
|
const ruleUntil = rule.until ? ScheduleModel.endOfDay(rule.until) : null;
|
||||||
|
|
||||||
const effectiveStart = new Date(Math.max(rangeStart.getTime(), seriesStart.getTime()));
|
const effectiveStart = new Date(Math.max(rangeStart.getTime(), seriesStart.getTime()));
|
||||||
let effectiveEnd = new Date(rangeEnd);
|
let effectiveEnd = new Date(rangeEnd);
|
||||||
if (seriesUntil && seriesUntil < effectiveEnd) effectiveEnd = seriesUntil;
|
if (seriesUntil && seriesUntil < effectiveEnd) effectiveEnd = seriesUntil;
|
||||||
if (ruleUntil && ruleUntil < effectiveEnd) effectiveEnd = ruleUntil;
|
if (ruleUntil && ruleUntil < effectiveEnd) effectiveEnd = ruleUntil;
|
||||||
if (effectiveStart > effectiveEnd) return [];
|
if (effectiveStart > effectiveEnd) return [];
|
||||||
|
|
||||||
if (!rule.freq) {
|
if (!rule.freq) {
|
||||||
return seriesStart >= effectiveStart && seriesStart <= effectiveEnd ? [seriesStart] : [];
|
return seriesStart >= effectiveStart && seriesStart <= effectiveEnd ? [seriesStart] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const dates: Date[] = [];
|
const dates: Date[] = [];
|
||||||
let generatedCount = 0;
|
let generatedCount = 0;
|
||||||
let current = new Date(seriesStart);
|
let current = new Date(seriesStart);
|
||||||
const byDay = rule.byDay?.length ? rule.byDay : [ScheduleModel.weekdayToRRule(seriesStart.getDay())];
|
const byDay = rule.byDay?.length ? rule.byDay : [ScheduleModel.weekdayToRRule(seriesStart.getDay())];
|
||||||
const interval = rule.interval || 1;
|
const interval = rule.interval || 1;
|
||||||
|
|
||||||
while (current <= effectiveEnd) {
|
while (current <= effectiveEnd) {
|
||||||
if (ScheduleModel.matchesRule(current, seriesStart, rule.freq, interval, byDay)) {
|
if (ScheduleModel.matchesRule(current, seriesStart, rule.freq, interval, byDay)) {
|
||||||
generatedCount += 1;
|
generatedCount += 1;
|
||||||
if (current >= effectiveStart) {
|
if (current >= effectiveStart) {
|
||||||
dates.push(new Date(current));
|
dates.push(new Date(current));
|
||||||
}
|
}
|
||||||
if (rule.count && generatedCount >= rule.count) break;
|
if (rule.count && generatedCount >= rule.count) break;
|
||||||
}
|
}
|
||||||
current.setDate(current.getDate() + 1);
|
current.setDate(current.getDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return dates;
|
return dates;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static matchesRule(date: Date, start: Date, freq: string, interval: number, byDay: string[]): boolean {
|
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);
|
const diffDays = Math.floor((ScheduleModel.startOfDay(date).getTime() - ScheduleModel.startOfDay(start).getTime()) / 86400000);
|
||||||
if (diffDays < 0) return false;
|
if (diffDays < 0) return false;
|
||||||
|
|
||||||
if (freq === "DAILY") return diffDays % interval === 0;
|
if (freq === "DAILY") return diffDays % interval === 0;
|
||||||
if (freq === "WEEKLY") {
|
if (freq === "WEEKLY") {
|
||||||
const weekIndex = Math.floor(diffDays / 7);
|
const weekIndex = Math.floor(diffDays / 7);
|
||||||
return weekIndex % interval === 0 && byDay.includes(ScheduleModel.weekdayToRRule(date.getDay()));
|
return weekIndex % interval === 0 && byDay.includes(ScheduleModel.weekdayToRRule(date.getDay()));
|
||||||
}
|
}
|
||||||
if (freq === "MONTHLY") {
|
if (freq === "MONTHLY") {
|
||||||
const months = (date.getFullYear() - start.getFullYear()) * 12 + (date.getMonth() - start.getMonth());
|
const months = (date.getFullYear() - start.getFullYear()) * 12 + (date.getMonth() - start.getMonth());
|
||||||
return months % interval === 0 && date.getDate() === start.getDate();
|
return months % interval === 0 && date.getDate() === start.getDate();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static parseRRule(rule: string): { freq?: string; interval?: number; count?: number; until?: Date; byDay?: string[] } {
|
private static parseRRule(rule: string): { freq?: string; interval?: number; count?: number; until?: Date; byDay?: string[] } {
|
||||||
if (!rule) return {};
|
if (!rule) return {};
|
||||||
const pairs = rule.split(";").map((x) => x.trim()).filter(Boolean);
|
const pairs = rule.split(";").map((x) => x.trim()).filter(Boolean);
|
||||||
const out: { [key: string]: string } = {};
|
const out: { [key: string]: string } = {};
|
||||||
for (const p of pairs) {
|
for (const p of pairs) {
|
||||||
const [k, v] = p.split("=");
|
const [k, v] = p.split("=");
|
||||||
if (k && v) out[k.toUpperCase()] = v;
|
if (k && v) out[k.toUpperCase()] = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
const untilRaw = out.UNTIL;
|
const untilRaw = out.UNTIL;
|
||||||
let untilDate: Date = null;
|
let untilDate: Date = null;
|
||||||
if (untilRaw) {
|
if (untilRaw) {
|
||||||
if (/^\d{8}$/.test(untilRaw)) {
|
if (/^\d{8}$/.test(untilRaw)) {
|
||||||
untilDate = new Date(`${untilRaw.slice(0, 4)}-${untilRaw.slice(4, 6)}-${untilRaw.slice(6, 8)}T00:00:00`);
|
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)) {
|
} 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`);
|
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 {
|
} else {
|
||||||
untilDate = new Date(untilRaw);
|
untilDate = new Date(untilRaw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
freq: out.FREQ?.toUpperCase(),
|
freq: out.FREQ?.toUpperCase(),
|
||||||
interval: out.INTERVAL ? Number(out.INTERVAL) : 1,
|
interval: out.INTERVAL ? Number(out.INTERVAL) : 1,
|
||||||
count: out.COUNT ? Number(out.COUNT) : undefined,
|
count: out.COUNT ? Number(out.COUNT) : undefined,
|
||||||
until: untilDate && !isNaN(untilDate.getTime()) ? untilDate : undefined,
|
until: untilDate && !isNaN(untilDate.getTime()) ? untilDate : undefined,
|
||||||
byDay: out.BYDAY ? out.BYDAY.split(",").map((x) => x.trim().toUpperCase()) : undefined,
|
byDay: out.BYDAY ? out.BYDAY.split(",").map((x) => x.trim().toUpperCase()) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static combineDateAndTime(ymd: string, hhmmss: string): Date {
|
private static combineDateAndTime(ymd: string, hhmmss: string): Date {
|
||||||
return new Date(`${ymd}T${hhmmss}`);
|
return new Date(`${ymd}T${hhmmss}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static startOfDay(date: Date): Date {
|
private static startOfDay(date: Date): Date {
|
||||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static endOfDay(date: Date): Date {
|
private static endOfDay(date: Date): Date {
|
||||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static formatYmd(date: Date): string {
|
private static formatYmd(date: Date): string {
|
||||||
const y = date.getFullYear();
|
const y = date.getFullYear();
|
||||||
const m = String(date.getMonth() + 1).padStart(2, "0");
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
const d = String(date.getDate()).padStart(2, "0");
|
const d = String(date.getDate()).padStart(2, "0");
|
||||||
return `${y}-${m}-${d}`;
|
return `${y}-${m}-${d}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static weekdayToRRule(day: number): string {
|
private static weekdayToRRule(day: number): string {
|
||||||
return ["SU", "MO", "TU", "WE", "TH", "FR", "SA"][day];
|
return ["SU", "MO", "TU", "WE", "TH", "FR", "SA"][day];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static recurrenceToRRule(recurrence: RecurrenceInput | null): string {
|
private static recurrenceToRRule(recurrence: RecurrenceInput | null): string {
|
||||||
if (!recurrence) return null;
|
if (!recurrence) return null;
|
||||||
const interval = recurrence.interval && recurrence.interval > 0 ? recurrence.interval : 1;
|
const interval = recurrence.interval && recurrence.interval > 0 ? recurrence.interval : 1;
|
||||||
if (recurrence.type === "weekly") {
|
if (recurrence.type === "weekly") {
|
||||||
const byDay = recurrence.daysOfWeek && recurrence.daysOfWeek.length > 0 ? recurrence.daysOfWeek.join(",") : "";
|
const byDay = recurrence.daysOfWeek && recurrence.daysOfWeek.length > 0 ? recurrence.daysOfWeek.join(",") : "";
|
||||||
return byDay ? `FREQ=WEEKLY;INTERVAL=${interval};BYDAY=${byDay}` : `FREQ=WEEKLY;INTERVAL=${interval}`;
|
return byDay ? `FREQ=WEEKLY;INTERVAL=${interval};BYDAY=${byDay}` : `FREQ=WEEKLY;INTERVAL=${interval}`;
|
||||||
}
|
}
|
||||||
if (recurrence.type === "daily") {
|
if (recurrence.type === "daily") {
|
||||||
return `FREQ=DAILY;INTERVAL=${interval}`;
|
return `FREQ=DAILY;INTERVAL=${interval}`;
|
||||||
}
|
}
|
||||||
if (recurrence.type === "monthly") {
|
if (recurrence.type === "monthly") {
|
||||||
return `FREQ=MONTHLY;INTERVAL=${interval}`;
|
return `FREQ=MONTHLY;INTERVAL=${interval}`;
|
||||||
}
|
}
|
||||||
throw new Error("Unsupported recurrence type");
|
throw new Error("Unsupported recurrence type");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user