diff --git a/src/controllers/schedule.ts b/src/controllers/schedule.ts index 80ac303..1a2177a 100644 --- a/src/controllers/schedule.ts +++ b/src/controllers/schedule.ts @@ -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(), diff --git a/src/model/schedule.ts b/src/model/schedule.ts index f100db0..e565062 100644 --- a/src/model/schedule.ts +++ b/src/model/schedule.ts @@ -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> { + const usedQuotaMap = new Map(); + 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; + } } diff --git a/src/swagger/builder.js b/src/swagger/builder.js index 619493d..022d920 100644 --- a/src/swagger/builder.js +++ b/src/swagger/builder.js @@ -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"