feat(api): support schedule quota tracking

This commit is contained in:
avicenan
2026-03-25 11:17:52 +07:00
parent 88e715c592
commit c3f77f4c69
3 changed files with 51 additions and 0 deletions

View File

@ -234,6 +234,7 @@ export class ScheduleController {
doctorId: Joi.string().uuid().required(), doctorId: Joi.string().uuid().required(),
title: Joi.string().max(256).required(), title: Joi.string().max(256).required(),
timezone: Joi.string().max(64).required(), timezone: Joi.string().max(64).required(),
quota: Joi.number().integer().min(0).allow(null).optional(),
startDate: Joi.string().required(), startDate: Joi.string().required(),
untilDate: Joi.string().allow("", null).optional(), untilDate: Joi.string().allow("", null).optional(),
startTimeLocal: Joi.string().required(), startTimeLocal: Joi.string().required(),
@ -277,6 +278,7 @@ export class ScheduleController {
doctorId: Joi.string().uuid().required(), doctorId: Joi.string().uuid().required(),
title: Joi.string().max(256).required(), title: Joi.string().max(256).required(),
timezone: Joi.string().max(64).required(), timezone: Joi.string().max(64).required(),
quota: Joi.number().integer().min(0).allow(null).optional(),
startDate: Joi.string().required(), startDate: Joi.string().required(),
untilDate: Joi.string().allow("", null).optional(), untilDate: Joi.string().allow("", null).optional(),
startTimeLocal: Joi.string().required(), startTimeLocal: Joi.string().required(),

View File

@ -14,6 +14,7 @@ type ScheduleSeriesPayload = {
doctorId: string; doctorId: string;
title: string; title: string;
timezone: string; timezone: string;
quota?: number | null;
startDate: string; startDate: string;
untilDate?: string | null; untilDate?: string | null;
startTimeLocal: string; startTimeLocal: string;
@ -82,6 +83,7 @@ export class ScheduleModel {
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 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")
@ -132,6 +134,12 @@ export class ScheduleModel {
title: eventTitle, title: eventTitle,
startAt: eventStart.toISOString(), startAt: eventStart.toISOString(),
endAt: eventEnd.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, isRecurring: !!series.recurrence_rule,
isException: !!exception, isException: !!exception,
status: "confirmed", status: "confirmed",
@ -246,6 +254,7 @@ export class ScheduleModel {
series.doctor = doctor; series.doctor = doctor;
series.title = payload.title; series.title = payload.title;
series.timezone = payload.timezone; series.timezone = payload.timezone;
series.quota = payload.quota ?? null;
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;
@ -278,6 +287,7 @@ export class ScheduleModel {
series.doctor = doctor; series.doctor = doctor;
series.title = payload.title; series.title = payload.title;
series.timezone = payload.timezone; series.timezone = payload.timezone;
series.quota = payload.quota ?? null;
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;
@ -455,4 +465,38 @@ export class ScheduleModel {
} }
throw new Error("Unsupported recurrence type"); 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;
}
} }

View File

@ -366,6 +366,7 @@ const doc = {
$doctorId: "uuid-string", $doctorId: "uuid-string",
$title: "Practice Schedule", $title: "Practice Schedule",
$timezone: "Asia/Jakarta", $timezone: "Asia/Jakarta",
$quota: 20,
$startDate: "2026-03-20", $startDate: "2026-03-20",
$untilDate: "2026-06-30", $untilDate: "2026-06-30",
$startTimeLocal: "09:00:00", $startTimeLocal: "09:00:00",
@ -377,6 +378,7 @@ const doc = {
$doctorId: "uuid-string", $doctorId: "uuid-string",
$title: "Practice Schedule Updated", $title: "Practice Schedule Updated",
$timezone: "Asia/Jakarta", $timezone: "Asia/Jakarta",
$quota: 25,
$startDate: "2026-03-20", $startDate: "2026-03-20",
$untilDate: "2026-07-31", $untilDate: "2026-07-31",
$startTimeLocal: "10:00:00", $startTimeLocal: "10:00:00",
@ -406,6 +408,9 @@ const doc = {
$title: "Practice", $title: "Practice",
$startAt: "2026-03-20T09:00:00+07:00", $startAt: "2026-03-20T09:00:00+07:00",
$endAt: "2026-03-20T12:00:00+07:00", $endAt: "2026-03-20T12:00:00+07:00",
$quota: 20,
$usedQuota: 7,
$leftQuota: 13,
$isRecurring: true, $isRecurring: true,
$isException: false, $isException: false,
$status: "confirmed" $status: "confirmed"