feat(api): support schedule quota tracking
This commit is contained in:
@ -234,6 +234,7 @@ export class ScheduleController {
|
||||
doctorId: Joi.string().uuid().required(),
|
||||
title: Joi.string().max(256).required(),
|
||||
timezone: Joi.string().max(64).required(),
|
||||
quota: Joi.number().integer().min(0).allow(null).optional(),
|
||||
startDate: Joi.string().required(),
|
||||
untilDate: Joi.string().allow("", null).optional(),
|
||||
startTimeLocal: Joi.string().required(),
|
||||
@ -277,6 +278,7 @@ export class ScheduleController {
|
||||
doctorId: Joi.string().uuid().required(),
|
||||
title: Joi.string().max(256).required(),
|
||||
timezone: Joi.string().max(64).required(),
|
||||
quota: Joi.number().integer().min(0).allow(null).optional(),
|
||||
startDate: Joi.string().required(),
|
||||
untilDate: Joi.string().allow("", null).optional(),
|
||||
startTimeLocal: Joi.string().required(),
|
||||
|
||||
@ -14,6 +14,7 @@ type ScheduleSeriesPayload = {
|
||||
doctorId: string;
|
||||
title: string;
|
||||
timezone: string;
|
||||
quota?: number | null;
|
||||
startDate: string;
|
||||
untilDate?: string | null;
|
||||
startTimeLocal: string;
|
||||
@ -82,6 +83,7 @@ export class ScheduleModel {
|
||||
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")
|
||||
@ -132,6 +134,12 @@ export class ScheduleModel {
|
||||
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",
|
||||
@ -246,6 +254,7 @@ export class ScheduleModel {
|
||||
series.doctor = doctor;
|
||||
series.title = payload.title;
|
||||
series.timezone = payload.timezone;
|
||||
series.quota = payload.quota ?? null;
|
||||
series.start_date = new Date(payload.startDate);
|
||||
series.until_date = payload.untilDate ? new Date(payload.untilDate) : null;
|
||||
series.start_time_local = payload.startTimeLocal;
|
||||
@ -278,6 +287,7 @@ export class ScheduleModel {
|
||||
series.doctor = doctor;
|
||||
series.title = payload.title;
|
||||
series.timezone = payload.timezone;
|
||||
series.quota = payload.quota ?? null;
|
||||
series.start_date = new Date(payload.startDate);
|
||||
series.until_date = payload.untilDate ? new Date(payload.untilDate) : null;
|
||||
series.start_time_local = payload.startTimeLocal;
|
||||
@ -455,4 +465,38 @@ export class ScheduleModel {
|
||||
}
|
||||
throw new Error("Unsupported recurrence type");
|
||||
}
|
||||
|
||||
private static async getDailyUsedQuotaMap(seriesIds: string[], rangeStart: Date, rangeEnd: Date): Promise<Map<string, number>> {
|
||||
const usedQuotaMap = new Map<string, number>();
|
||||
if (seriesIds.length === 0) return usedQuotaMap;
|
||||
|
||||
const placeholders = seriesIds.map((_, index) => `$${index + 1}`).join(", ");
|
||||
const startParamIndex = seriesIds.length + 1;
|
||||
const endParamIndex = seriesIds.length + 2;
|
||||
const params = [...seriesIds, ScheduleModel.formatYmd(rangeStart), ScheduleModel.formatYmd(rangeEnd)];
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
ro."scheduleSeriesId" AS series_id,
|
||||
DATE(ro.visit_date) AS visit_date,
|
||||
COUNT(ro.id) AS used_count
|
||||
FROM emr.emr_registration_outpatient ro
|
||||
WHERE ro."scheduleSeriesId" IN (${placeholders})
|
||||
AND DATE(ro.visit_date) BETWEEN $${startParamIndex} AND $${endParamIndex}
|
||||
AND ro.deleted_at IS NULL
|
||||
GROUP BY ro."scheduleSeriesId", DATE(ro.visit_date)
|
||||
`;
|
||||
|
||||
const rows: Array<{ series_id: string; visit_date: string | Date; used_count: string | number }> = await OrmHelper.DB.manager.query(
|
||||
query,
|
||||
params
|
||||
);
|
||||
|
||||
for (const row of rows) {
|
||||
const occurrenceYmd = ScheduleModel.formatYmd(new Date(row.visit_date));
|
||||
usedQuotaMap.set(`${row.series_id}:${occurrenceYmd}`, Number(row.used_count) || 0);
|
||||
}
|
||||
|
||||
return usedQuotaMap;
|
||||
}
|
||||
}
|
||||
|
||||
@ -366,6 +366,7 @@ const doc = {
|
||||
$doctorId: "uuid-string",
|
||||
$title: "Practice Schedule",
|
||||
$timezone: "Asia/Jakarta",
|
||||
$quota: 20,
|
||||
$startDate: "2026-03-20",
|
||||
$untilDate: "2026-06-30",
|
||||
$startTimeLocal: "09:00:00",
|
||||
@ -377,6 +378,7 @@ const doc = {
|
||||
$doctorId: "uuid-string",
|
||||
$title: "Practice Schedule Updated",
|
||||
$timezone: "Asia/Jakarta",
|
||||
$quota: 25,
|
||||
$startDate: "2026-03-20",
|
||||
$untilDate: "2026-07-31",
|
||||
$startTimeLocal: "10:00:00",
|
||||
@ -406,6 +408,9 @@ const doc = {
|
||||
$title: "Practice",
|
||||
$startAt: "2026-03-20T09:00:00+07:00",
|
||||
$endAt: "2026-03-20T12:00:00+07:00",
|
||||
$quota: 20,
|
||||
$usedQuota: 7,
|
||||
$leftQuota: 13,
|
||||
$isRecurring: true,
|
||||
$isException: false,
|
||||
$status: "confirmed"
|
||||
|
||||
Reference in New Issue
Block a user