fix(schedule): calendar
This commit is contained in:
@ -137,31 +137,45 @@ export class ScheduleController {
|
|||||||
#swagger.parameters['limit'] = { in: 'query', required: false, type: 'number' }
|
#swagger.parameters['limit'] = { in: 'query', required: false, type: 'number' }
|
||||||
#swagger.parameters['page'] = { in: 'query', required: false, type: 'number' }
|
#swagger.parameters['page'] = { in: 'query', required: false, type: 'number' }
|
||||||
#swagger.responses[200] = {
|
#swagger.responses[200] = {
|
||||||
description: 'Schedule calendar resources and events',
|
description: 'Schedule calendar rooms and events',
|
||||||
schema: {
|
schema: {
|
||||||
status: true,
|
status: true,
|
||||||
code: 200,
|
code: 200,
|
||||||
message: 'success',
|
message: 'success',
|
||||||
data: {
|
data: {
|
||||||
resources: [{
|
count: 1,
|
||||||
id: 'room-1:doctor-1',
|
page: 1,
|
||||||
roomId: 'room-1',
|
total_count: 1,
|
||||||
roomName: 'Room 1',
|
list: {
|
||||||
doctorId: 'doctor-1',
|
rooms: [{
|
||||||
doctorName: 'Dr A'
|
id: 'uuid-string',
|
||||||
}],
|
name: 'string',
|
||||||
events: [{
|
code: 'string',
|
||||||
id: 'occ:series-101:2026-03-20',
|
picture: 'string',
|
||||||
seriesId: 'series-101',
|
location: 'string',
|
||||||
resourceId: 'room-1:doctor-1',
|
status: 'string',
|
||||||
title: 'Practice',
|
department: {
|
||||||
startAt: '2026-03-20T09:00:00+07:00',
|
name: 'string',
|
||||||
endAt: '2026-03-20T12:00:00+07:00',
|
},
|
||||||
isRecurring: true,
|
}],
|
||||||
isException: false,
|
events: [{
|
||||||
status: 'confirmed'
|
id: 'occ:series-101:2026-03-20',
|
||||||
}],
|
series_id: 'series-101',
|
||||||
meta: { total_count: 24, page: 1, limit: 20 }
|
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'
|
||||||
|
}],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { Room, ScheduleException, ScheduleSeries, Status, User } from "entity";
|
import { Room, ScheduleException, ScheduleSeries, Status, User } from "entity";
|
||||||
import { SelectQueryBuilder } from "typeorm";
|
import { IsNull, SelectQueryBuilder } from "typeorm";
|
||||||
import CommonHelper from "../helpers/common";
|
import CommonHelper from "../helpers/common";
|
||||||
import { OrmHelper } from "../helpers/orm";
|
import { OrmHelper } from "../helpers/orm";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { RoomModel } from "./room";
|
||||||
|
|
||||||
type RecurrenceInput = {
|
type RecurrenceInput = {
|
||||||
type: "daily" | "weekly" | "monthly";
|
type: "daily" | "weekly" | "monthly";
|
||||||
@ -57,32 +59,309 @@ export class ScheduleModel {
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// static async getScheduleCalendarRange(start: string, end: string, filterObj = {}, options: CalendarQueryOptions = {}): Promise<any> {
|
||||||
|
// 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<string, ScheduleException>();
|
||||||
|
// 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<string, any>();
|
||||||
|
// 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<any> {
|
||||||
|
// 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<string, ScheduleException>();
|
||||||
|
// 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<string, any>();
|
||||||
|
// 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<any> {
|
static async getScheduleCalendarRange(start: string, end: string, filterObj = {}, options: CalendarQueryOptions = {}): Promise<any> {
|
||||||
const startDate = new Date(start);
|
const startDate = dayjs(start);
|
||||||
const endDate = new Date(end);
|
const endDate = dayjs(end);
|
||||||
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
if (!startDate.isValid() || !endDate.isValid()) {
|
||||||
throw new Error("Invalid start or end date");
|
throw new Error("Invalid start or end date");
|
||||||
}
|
}
|
||||||
|
|
||||||
const rangeStart = ScheduleModel.startOfDay(startDate);
|
const rangeStart = startDate.startOf("day").toDate();
|
||||||
const rangeEnd = ScheduleModel.endOfDay(endDate);
|
const rangeEnd = endDate.endOf("day").toDate();
|
||||||
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 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);
|
const seriesQuery = await ScheduleModel.listSeries(filterObj);
|
||||||
seriesQuery
|
const seriesList = await seriesQuery
|
||||||
.andWhere("ScheduleSeries.start_date <= :rangeEndDate", { rangeEndDate: ScheduleModel.formatYmd(rangeEnd) })
|
.where("ScheduleSeries.room_id IN (:...roomIds)", { roomIds })
|
||||||
.andWhere("(ScheduleSeries.until_date IS NULL OR ScheduleSeries.until_date >= :rangeStartDate)", {
|
.andWhere("ScheduleSeries.start_date <= :rangeEndDate", { rangeEndDate: dayjs(rangeEnd).format("YYYY-MM-DD") })
|
||||||
rangeStartDate: ScheduleModel.formatYmd(rangeStart),
|
.andWhere("(ScheduleSeries.until_date IS NULL OR ScheduleSeries.until_date >= :rangeStartDate)", { rangeStartDate: dayjs(rangeStart).format("YYYY-MM-DD") })
|
||||||
})
|
.andWhere("ScheduleSeries.deleted_at IS NULL")
|
||||||
.andWhere("ScheduleSeries.deleted_at IS NULL");
|
.getMany();
|
||||||
|
|
||||||
const seriesList: ScheduleSeries[] = await seriesQuery.getMany();
|
|
||||||
if (seriesList.length === 0) return { resources: [], events: [] };
|
|
||||||
|
|
||||||
|
if (seriesList.length === 0) return { rooms: rooms, events: [], meta: { total_count: rooms.length, page, limit } };
|
||||||
const seriesIds = seriesList.map((series) => series.id);
|
const seriesIds = seriesList.map((series) => series.id);
|
||||||
const usageMap = await ScheduleModel.getDailyUsedQuotaMap(seriesIds, rangeStart, rangeEnd);
|
const usageMap = await ScheduleModel.getDailyUsedQuotaMap(seriesIds, rangeStart, rangeEnd);
|
||||||
|
|
||||||
const exceptionRepo = OrmHelper.DB.getRepository(ScheduleException);
|
const exceptionRepo = OrmHelper.DB.getRepository(ScheduleException);
|
||||||
const exceptions = await exceptionRepo
|
const exceptions = await exceptionRepo
|
||||||
.createQueryBuilder("ScheduleException")
|
.createQueryBuilder("ScheduleException")
|
||||||
@ -98,7 +377,7 @@ export class ScheduleModel {
|
|||||||
|
|
||||||
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}:${dayjs(ex.occurrence_date).format("YYYY-MM-DD")}`;
|
||||||
exceptionMap.set(key, ex);
|
exceptionMap.set(key, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +386,7 @@ export class ScheduleModel {
|
|||||||
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 = dayjs(occurrenceDate).format("YYYY-MM-DD");
|
||||||
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;
|
||||||
|
|
||||||
@ -122,68 +401,40 @@ export class ScheduleModel {
|
|||||||
|
|
||||||
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 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,
|
series_id: series.id,
|
||||||
resourceId,
|
|
||||||
title: eventTitle,
|
title: eventTitle,
|
||||||
startAt: eventStart.toISOString(),
|
room_id: roomId,
|
||||||
endAt: eventEnd.toISOString(),
|
room_name: roomName,
|
||||||
|
doctor_id: doctorId,
|
||||||
|
doctor_name: doctorName,
|
||||||
|
start_at: eventStart.toISOString(),
|
||||||
|
end_at: eventEnd.toISOString(),
|
||||||
quota: series.quota ?? null,
|
quota: series.quota ?? null,
|
||||||
usedQuota: usageMap.get(`${series.id}:${occurrenceYmd}`) || 0,
|
used_quota: usageMap.get(`${series.id}:${occurrenceYmd}`) || 0,
|
||||||
leftQuota:
|
left_quota:
|
||||||
typeof series.quota === "number"
|
typeof series.quota === "number"
|
||||||
? Math.max(0, series.quota - (usageMap.get(`${series.id}:${occurrenceYmd}`) || 0))
|
? Math.max(0, series.quota - (usageMap.get(`${series.id}:${occurrenceYmd}`) || 0))
|
||||||
: null,
|
: null,
|
||||||
isRecurring: !!series.recurrence_rule,
|
is_recurring: !!series.recurrence_rule,
|
||||||
isException: !!exception,
|
is_exception: !!exception,
|
||||||
status: "confirmed",
|
status: "confirmed",
|
||||||
_resource: {
|
|
||||||
id: resourceId,
|
|
||||||
roomId,
|
|
||||||
roomName,
|
|
||||||
doctorId,
|
|
||||||
doctorName,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allResourcesMap = new Map<string, any>();
|
|
||||||
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 {
|
return {
|
||||||
resources: pagedResources,
|
count: rooms.length,
|
||||||
events: pagedEvents,
|
page: page,
|
||||||
meta: {
|
total_count: roomTotalCount,
|
||||||
total_count: totalCount,
|
list: {
|
||||||
page,
|
rooms: rooms,
|
||||||
limit,
|
events: allEvents,
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user