Standing Orders (RRULE)
RRULE-based recurring/future order templates that auto-surface at patient check-in.
Master execution plan for recurring/future order templates that auto-surface at patient check-in.
1. Concept
A standing order is a clinician’s intent stored with an RRULE recurrence rule. It is NOT an active order — it becomes one only when a nurse explicitly activates it (or auto-fires for ICU protocols). On each encounter, the system evaluates active standing orders for the patient and presents matching ones in the screening modal for confirmation.
Doctor writes standing order (once)
↓
Stored in Supabase: standing_orders table
↓
Patient checks in on a day matching RRULE
↓
Screening modal shows: "These standing orders match today"
↓
Nurse confirms (checkbox) → "Activate"
↓
createOrderRequestApi() → normal order pipeline
(allergy check, dept queue, billing, eMAR — unchanged)
↓
standing_order.occurrence_count++
2. Data Model
2.1 standing_orders table
create table standing_orders (
id uuid primary key default gen_random_uuid(),
-- ── Patient & context ──
patient_id text not null, -- MongoDB patient._id
patient_hn text, -- Denormalized for display
patient_name text, -- Denormalized for display
-- ── Order template ──
order_type text not null, -- 'medication' | 'lab' | 'imaging' | 'procedure' | 'nutrition' | 'blood_product'
order_template jsonb not null, -- CreateOrderRequestDto.listitems[]-shaped payload (array of items)
order_summary text not null, -- Human-readable: "CBC + Urinalysis" or "Metformin 500mg BID"
priority text not null default 'ROUTINE', -- ROUTINE | URGENT | STAT
category text not null default 'OUTPATIENT', -- OUTPATIENT | INPATIENT | DISCHARGE
-- ── Recurrence (RRULE) ──
rrule text not null, -- iCal RRULE string (e.g. "FREQ=WEEKLY;BYDAY=MO;COUNT=8")
dtstart timestamptz not null, -- Recurrence start (first eligible date)
until_date timestamptz, -- Hard stop (nullable = no end, relies on max_occurrences or manual cancel)
max_occurrences int, -- Max times to activate (nullable = unlimited until until_date)
occurrence_count int not null default 0, -- How many times activated so far
-- ── Activation mode ──
activation_mode text not null default 'confirm', -- 'confirm' (nurse must tick) | 'auto' (fires on check-in without prompt)
-- ── Signing & authorization ──
created_by text not null, -- User._id of ordering clinician
created_by_name text, -- Denormalized display name
created_by_license text, -- License number (ว./ท.พ./etc.)
signed_at timestamptz not null default now(),-- When clinician authorized this standing order
co_signer_id text, -- Attending physician (if resident ordered)
co_signer_name text,
co_signer_license text,
co_signed_at timestamptz, -- null = pending co-sign
co_sign_required boolean not null default false,
-- ── Periodic review ──
review_interval_days int default 90, -- Standing orders must be reviewed every N days (regulatory)
last_reviewed_at timestamptz, -- null = never reviewed (use signed_at as baseline)
last_reviewed_by text, -- User._id of reviewer
next_review_due timestamptz, -- Computed: last_reviewed_at + review_interval_days
review_status text not null default 'current', -- 'current' | 'due' | 'overdue' | 'expired_no_review'
-- ── Lifecycle ──
status text not null default 'active', -- 'active' | 'paused' | 'completed' | 'cancelled' | 'expired'
status_reason text, -- Why paused/cancelled (free text)
status_changed_by text, -- Who changed status
status_changed_at timestamptz,
-- ── Activation tracking ──
last_activated_at timestamptz,
last_activated_encounter_id text, -- Idempotency: don't activate twice in same encounter
activation_log jsonb not null default '[]', -- Array of { encounter_id, activated_at, activated_by, order_request_id }
-- ── Clinical context ──
indication text, -- Clinical reason / diagnosis
icd10_codes text[], -- Associated diagnoses
instructions text, -- Special instructions for activating nurse
contraindication_notes text, -- When NOT to activate (e.g. "hold if K+ > 5.5")
-- ── Source reference ──
source_encounter_id text, -- Encounter where this was created
source_order_request_id text, -- Original order it was derived from (if converted from one-time)
-- ── Metadata ──
facility_id text, -- Multi-facility support
department_id text, -- Originating department
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
-- ── Constraints ──
constraint valid_status check (status in ('active', 'paused', 'completed', 'cancelled', 'expired')),
constraint valid_activation_mode check (activation_mode in ('confirm', 'auto')),
constraint valid_review_status check (review_status in ('current', 'due', 'overdue', 'expired_no_review')),
constraint valid_order_type check (order_type in ('medication', 'lab', 'imaging', 'procedure', 'nutrition', 'blood_product', 'nursing'))
);
-- ── Indexes ──
create index idx_standing_orders_patient on standing_orders (patient_id, status);
create index idx_standing_orders_review on standing_orders (review_status, next_review_due) where status = 'active';
create index idx_standing_orders_cosign on standing_orders (co_sign_required, co_signed_at) where co_sign_required = true and co_signed_at is null;
-- ── Auto-update updated_at ──
create trigger trg_standing_orders_updated_at
before update on standing_orders
for each row execute function moddatetime(updated_at);
-- ── Auto-compute next_review_due ──
create or replace function compute_next_review_due()
returns trigger as $$
begin
new.next_review_due := coalesce(new.last_reviewed_at, new.signed_at) + (new.review_interval_days || ' days')::interval;
if new.next_review_due < now() and new.status = 'active' then
new.review_status := case
when new.next_review_due + interval '14 days' < now() then 'overdue'
else 'due'
end;
else
new.review_status := 'current';
end if;
return new;
end;
$$ language plpgsql;
create trigger trg_standing_orders_review_compute
before insert or update of last_reviewed_at, review_interval_days, signed_at
on standing_orders
for each row execute function compute_next_review_due();
-- ── Auto-expire completed orders ──
create or replace function check_standing_order_completion()
returns trigger as $$
begin
if new.max_occurrences is not null and new.occurrence_count >= new.max_occurrences then
new.status := 'completed';
new.status_reason := 'Max occurrences reached (' || new.max_occurrences || ')';
new.status_changed_at := now();
end if;
if new.until_date is not null and now() > new.until_date and new.status = 'active' then
new.status := 'expired';
new.status_reason := 'Past until_date';
new.status_changed_at := now();
end if;
return new;
end;
$$ language plpgsql;
create trigger trg_standing_orders_completion
before update of occurrence_count on standing_orders
for each row execute function check_standing_order_completion();
2.2 RLS Policies
-- All authenticated users can read standing orders for patients they have access to
alter table standing_orders enable row level security;
create policy "Clinicians can view standing orders"
on standing_orders for select
using (auth.role() = 'authenticated');
create policy "Clinicians can create standing orders"
on standing_orders for insert
with check (auth.role() = 'authenticated');
create policy "Clinicians can update standing orders"
on standing_orders for update
using (auth.role() = 'authenticated');
-- No deletes — status lifecycle only
2.3 order_template JSONB Shape
Matches the existing CreateOrderRequestDto.listitems[] format so activation is a direct passthrough:
// order_template is an array of items ready to drop into createOrderRequestApi
type OrderTemplate = Array<{
productid: string; // Product._id
product_name?: string; // Denormalized for display
product_code?: string; // Denormalized for display
qty: number;
dosage?: number;
dosageuom?: string;
uom?: string;
frequency?: string;
priority: 'ROUTINE' | 'URGENT' | 'STAT';
specimen?: string; // Lab
container?: string; // Lab
modalities?: string; // Imaging
administrationUsage?: { // Medication timing (within the order, not the standing order RRULE)
_id?: string;
name?: string;
rruleString?: string; // e.g. "FREQ=DAILY;BYHOUR=8,12,18,22"
startsOn?: string;
endDate?: string;
};
clinicalnote?: string;
instruction?: any;
prn?: boolean;
nutrition?: any; // Nutrition orders
device?: any; // Device orders
bloodProduct?: any; // Blood product orders
}>;
3. Signing & Authorization Rules
3.1 Creation Signing
| Scenario | Rule |
|---|---|
| Attending physician creates | signed_at = now(), co_sign_required = false |
| Resident creates | signed_at = now(), co_sign_required = true, co_signed_at = null |
| Nurse creates (protocol order) | signed_at = now(), co_sign_required = true (requires physician co-sign) |
Standing orders with co_sign_required = true AND co_signed_at IS NULL are in “pending co-sign” state:
- They appear in the co-signer’s inbox (reuse
AcknowledgementRequestsystem) - They CAN be activated by nurses (standing orders are pre-authorized by definition) but display a ⚠️ badge
- Co-sign must happen within 24h per most regulatory frameworks
3.2 Activation Signing
When nurse clicks “Activate”:
activated_by(nurse user._id) is recorded inactivation_log- The resulting
OrderRequesthasrequester = original_doctor(not the nurse) - The nurse is recorded as the person who confirmed clinical appropriateness at activation time
- If
activation_mode = 'auto',activated_by = 'SYSTEM'
3.3 Periodic Review Signing
Standing orders MUST be reviewed periodically (default: 90 days). This is a regulatory requirement in most jurisdictions (Joint Commission, Thai Medical Council, etc.).
| Review Status | Meaning | UI Treatment |
|---|---|---|
current |
Within review window | Normal display |
due |
Past next_review_due but < 14 days overdue |
Yellow badge, still activatable |
overdue |
Past next_review_due + 14 days |
Red badge, blocks activation until reviewed |
expired_no_review |
30+ days overdue → auto-paused | Greyed out, requires renewal |
Review action: doctor opens standing order → clicks “Review & Renew” → updates last_reviewed_at, last_reviewed_by, resets review_status = 'current'.
3.4 Discontinuation Signing
Any physician can discontinue (cancel) a standing order. Records:
status = 'cancelled'status_reason(required free text)status_changed_bystatus_changed_at
4. RRULE Evaluation Logic (Frontend)
import { RRule, rrulestr } from 'rrule';
interface StandingOrderMatch {
standingOrder: StandingOrder;
nextOccurrence: Date;
occurrenceNumber: number; // e.g. "3 of 8"
}
function evaluateStandingOrders(
orders: StandingOrder[],
encounterDate: Date = new Date()
): StandingOrderMatch[] {
const startOfDay = new Date(encounterDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(encounterDate);
endOfDay.setHours(23, 59, 59, 999);
return orders
.filter(o => o.status === 'active')
.filter(o => !o.co_sign_required || o.co_signed_at) // skip unsigned (or show with warning)
.filter(o => o.review_status !== 'overdue') // block overdue reviews
.map(order => {
const rule = rrulestr(`DTSTART:${toRRuleDateStr(order.dtstart)}\nRRULE:${order.rrule}`);
const matches = rule.between(startOfDay, endOfDay, true);
if (matches.length === 0) return null;
return {
standingOrder: order,
nextOccurrence: matches[0],
occurrenceNumber: order.occurrence_count + 1,
};
})
.filter(Boolean) as StandingOrderMatch[];
}
5. Activation Flow (Frontend → Backend)
async function activateStandingOrders(
selectedOrders: StandingOrderMatch[],
encounterId: string,
patientId: string,
activatedBy: string, // current user._id
) {
for (const match of selectedOrders) {
const { standingOrder } = match;
// 1. Idempotency check
if (standingOrder.last_activated_encounter_id === encounterId) {
console.warn(`Standing order ${standingOrder.id} already activated for this encounter`);
continue;
}
// 2. Build CreateOrderRequestDto from template
const orderPayload: IFormOrderRequest = {
encounter: encounterId,
patient: patientId,
status: 'pending',
category: standingOrder.category,
requester: standingOrder.created_by, // Original ordering physician
remark: `Standing Order #${standingOrder.id} — occurrence ${match.occurrenceNumber}`,
serialnumber: '',
ispaid: false,
saleorderstatus: 'NONE',
newSaleOrder: false,
doctorRef: standingOrder.created_by,
clinicRef: standingOrder.department_id || '',
isHomeMed: false,
isMedReconcile: false,
isMedicationSupply: false,
listitems: standingOrder.order_template.map(item => ({
isDoctorAssign: true,
isCancel: false,
orderrequestitem: null,
productid: item.productid,
qty: item.qty,
uniprice: 0, // Backend resolves from product master
priority: item.priority || standingOrder.priority,
remark: item.clinicalnote || null,
specimen: item.specimen || null,
container: item.container || null,
position: null,
prelim: false,
uom: item.uom || null,
dosageuom: item.dosageuom || null,
frequency: item.frequency || null,
administrationUsage: item.administrationUsage || null,
clinicalnote: item.clinicalnote || null,
modalities: item.modalities || null,
dosage: item.dosage,
reasonAllergy: undefined,
reasonInteractions: undefined,
reasonDuplicate: undefined,
})),
isBillable: true,
};
// 3. Fire through existing pipeline (allergy checks run here)
const [response] = await createOrderRequestApi(orderPayload);
const result = await response;
// 4. Update standing order tracking
const newLog = [
...standingOrder.activation_log,
{
encounter_id: encounterId,
activated_at: new Date().toISOString(),
activated_by: activatedBy,
order_request_id: result.data._id,
},
];
await supabase.from('standing_orders').update({
occurrence_count: standingOrder.occurrence_count + 1,
last_activated_at: new Date().toISOString(),
last_activated_encounter_id: encounterId,
activation_log: newLog,
}).eq('id', standingOrder.id);
}
}
6. Frontend Architecture — Where Components Live
6.0 Existing Order Surfaces (Map)
The current order ecosystem has these surfaces — standing orders must integrate with, not duplicate, them:
┌─────────────────────────────────────────────────────────────────────────────┐
│ PATIENT PROFILE (DynamicContentRenderer) │
│ ───────────────────────────────────────── │
│ Tab-based module system, rendered via DynamicCoreApp enum constants. │
│ Each "tab" is a miniapp or medical-kit component. │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ ORDER_SYSTEM │ │ ENHANCED_ORDER_VIEW │ │ ORDER_HISTORY_ │ │
│ │ (OrderApril2025) │ │ (read-only grouped) │ │ REORDER │ │
│ │ Full order entry │ │ By category: drug, │ │ Historical orders │ │
│ │ dialog + tabs │ │ lab, imaging, etc. │ │ + "reorder" action │ │
│ └─────────────────────┘ └─────────────────────┘ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ DOCTOR_ORDER │ │ ENHANCED_MEDICATION │ │ ★ STANDING_ORDERS │ │
│ │ IPD: "One Day" + │ │ _ORDER │ │ (NEW — this feature)│ │
│ │ "Continuation" │ │ Medication-specific │ │ Patient's active + │ │
│ │ columns │ │ order entry │ │ paused + history │ │
│ └─────────────────────┘ └─────────────────────┘ └─────────────────────┘ │
│ │
│ WidgetRail (side dock): │
│ ┌──────────────────────────────────────┐ │
│ │ OrderActivityDock — live status of │ │
│ │ active orders (WAITING/IN_PROGRESS/ │ │
│ │ COMPLETED) with queue numbers │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ SCREENING MODAL (ScreeningModal / service-kit) │
│ ────────────────────────────────────────────── │
│ Opened by nurse at OPD check-in. Tab-based via ScreeningTabId registry. │
│ Receives: patientId, encounterId, patientData, encounterData, status │
│ │
│ 26 tabs total. Key ones for standing orders: │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ ORDER │ │ ★ STANDING_ORDERS │ │
│ │ (PreOrderData) │ │ (NEW — this feature)│ │
│ │ Shows mock future │ │ RRULE-matched orders│ │
│ │ orders today │ │ + activate button │ │
│ │ ← REPLACE with real │ │ OR: merge into the │ │
│ │ standing order data│ │ existing ORDER tab │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ Other tabs: SCREENINGINFO, ALLERGY, DIAGNOSIS, VACCINE, PHYSICALEXAM, etc. │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ OPD PATIENT LIST (hospital-outpatient-patientlist) │
│ ────────────────────────────────────────────────── │
│ TableRowWaitCheckIn / TableRowAlreadyCheckIn │
│ │
│ Each row shows futureOrderLab badge: "2/3" (2 of 3 checked) │
│ ← Currently mock data. Will wire to standing_orders query. │
└─────────────────────────────────────────────────────────────────────────────┘
6.1 Decision: New Tab vs Replace Existing ORDER Tab
Recommendation: REPLACE the existing ORDER tab (PreOrderData.tsx)
Why:
PreOrderDatacurrently showsdataMockFutureOrder— it’s the exact placeholder for this feature- Adding a separate
STANDING_ORDERStab creates confusion (“what’s the difference?”) - The title is already “คำสั่งออเดอร์ล่วงหน้า” (Advance Order Commands) — that IS standing orders
- Keep one surface, one mental model for nurses
What changes:
PreOrderData.tsxstops importing mock → usesuseStandingOrders(patientId)hook- Accordion sections become: “วันนี้ — พร้อมดำเนินการ” (Today — Ready to activate) + “กำหนดการถัดไป” (Upcoming)
- “Active” button (currently commented out) gets wired to
activateStandingOrders()
6.2 Screening Modal Integration (Nurse OPD Flow)
Entry points (nurse opens screening modal from):
hospital-outpatient-screeninglist→ click patient row →DialogScreeningWorkflowConfigBasedTabs→ dispatchDIALOG_SCREENINGmodal
The standing order tab renders inside:
ScreeningModal
└─ SCREENING_TABS registry (tabs/registry.tsx)
└─ ScreeningTabId.ORDER
└─ loader: import('...patient-check-in/menu-tab/order/PreOrderData')
└─ <PreOrder> component
└─ useStandingOrders(patientId) ← NEW HOOK
└─ RRULE evaluation (client-side)
└─ AccordingFutureOrder (existing accordion)
└─ ListItemFutureOrder (existing checkbox table)
└─ "Activate" button → activateStandingOrders()
Props available (passed by ScreeningModal via componentProps):
{
patientId: string; // ✓ needed for query
encounterId: string; // ✓ needed for activation + idempotency
patientData: any; // ✓ has patient_hn, name
encounterData: any; // ✓ has encounter class, status
status: string; // screening status
handleClose: () => void;
}
All props needed for standing order activation are already available — no new prop threading.
6.3 Patient Profile Module (Doctor View)
Register as: DynamicCoreApp.STANDING_ORDERS = 'modules.StandingOrders'
Component: StandingOrderPanel — a full CRUD view for doctors:
┌─────────────────────────────────────────────────────────────┐
│ คำสั่งประจำ (Standing Orders) [+ สร้าง] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ● Active (2) │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ ☰ CBC + Electrolytes ││
│ │ ทุกวันจันทร์ • 2/8 ครั้ง • ถัดไป: 19 พ.ค. 2569 ││
│ │ Dr. Admin • ว.00001 • สร้าง 10 พ.ค. 2569 ││
│ │ [หยุดชั่วคราว] [ยกเลิก] [แก้ไข] ││
│ ├─────────────────────────────────────────────────────────┤│
│ │ ☰ Metformin 500mg BID ││
│ │ ทุกวันที่ 1 ของเดือน • 5/∞ ครั้ง • ถัดไป: 1 มิ.ย. ││
│ │ Dr. Admin • ว.00001 • ⚠️ ต้อง review (เกิน 90 วัน) ││
│ │ [Review & Renew] [หยุดชั่วคราว] [ยกเลิก] ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ ○ Paused (1) │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ ☰ Warfarin 5mg QD ││
│ │ หยุดชั่วคราว: "รอผล INR" • โดย Dr. Admin ││
│ │ [เปิดใช้งานอีกครั้ง] ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ ○ Completed / Cancelled (3) ▸ │
└─────────────────────────────────────────────────────────────┘
How it renders in patient profile:
// In DynamicContentRenderer.tsx (existing switch/case block)
case DynamicCoreApp.STANDING_ORDERS:
return <StandingOrderPanel patientId={patientRef} encounterId={lastEncounter?._id} />;
6.4 OrderActivityDock Sync (WidgetRail)
The OrderActivityDock widget shows real-time status of active orders (WAITING → IN_PROGRESS → COMPLETED). Standing orders DON’T appear here because they’re not active orders yet — but once activated, the resulting OrderRequest flows through the normal pipeline and shows up automatically.
No changes needed to OrderActivityDock. It already reads from department_queues which gets populated by handleOrderCreated in the orchestrator when createOrderRequestApi() fires.
6.5 OPD Patient List Badge
The futureOrderLab badge on patient rows (TableRowWaitCheckIn.tsx) currently shows mock data. Wire it to:
// In the patient list data fetcher
const { data: standingOrders } = useQuery(
['standing-orders-badge', patientId],
() => supabase
.from('standing_orders')
.select('id, rrule, dtstart, occurrence_count, max_occurrences')
.eq('patient_id', patientId)
.eq('status', 'active')
.then(r => r.data),
);
// Evaluate which match today
const todayMatches = evaluateStandingOrders(standingOrders || []);
// Badge: "2/3" = 2 ready to activate out of 3 total active
6.6 Doctor Order Dialog — “Save as Standing Order”
Where: Inside OrderApril2025.tsx / DialogOrder.tsx (the main order entry)
After filling the order form, doctor sees:
┌───────────────────────────────────────────────────┐
│ [x] บันทึกเป็นคำสั่งประจำ (Save as Standing Order)│
│ │
│ ┌─ RRULE Picker (appears on toggle) ──────────┐ │
│ │ ความถี่: ○ วัน ● สัปดาห์ ○ เดือน │ │
│ │ วัน: □จ ☑อ □พ □พฤ □ศ □ส □อา │ │
│ │ สิ้นสุด: ● หลังจาก [8] ครั้ง │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ [ส่ง Order ปัจจุบัน + สร้าง Standing Order] │
│ [สร้าง Standing Order อย่างเดียว (ไม่ส่งวันนี้)] │
└───────────────────────────────────────────────────┘
Two paths:
- Submit current + save standing → fires
createOrderRequestApi()for today AND insertsstanding_ordersfor future - Standing only → only inserts
standing_orders(for “start next week” scenarios)
6.7 Admin Dashboard — Standing Order Review
Where: /super-admin/standing-orders (new page in admin-kit)
For: Medical directors / chief physicians to review overdue standing orders across all patients.
6.8 Co-Sign Inbox
Where: Reuse existing AcknowledgementInbox FAB (already globally mounted in App.tsx)
Standing orders with co_sign_required = true AND co_signed_at IS NULL generate an acknowledgement request visible in the inbox. Doctor clicks → reviews → co-signs.
6.9 Component Ownership Summary
| Component | Package | Standalone? | Consumed By |
|---|---|---|---|
useStandingOrders hook |
web/src/hooks/ |
Yes | All below |
StandingOrdersTab (nurse activation) |
medical-kit/order/ |
Yes | ScreeningModal ORDER tab |
StandingOrderPanel (doctor CRUD) |
miniapps/standing-orders/ |
Yes | Patient profile module |
RRulePicker |
ui-kit/ |
Yes | Order dialog, StandingOrderPanel |
StandingOrderBadge |
ui-kit/ |
Yes | OPD patient list rows |
StandingOrderReviewDashboard |
admin-kit/ |
Yes | Super-admin page |
activateStandingOrders util |
web/src/services/medbase/ |
Yes | StandingOrdersTab, auto-activate fn |
All components are standalone — they share one hook (useStandingOrders) and one service (standing-orders.service.ts) but render independently. No tight coupling between the nurse view and doctor view.
7. RRULE Picker Component
A reusable RRULE builder for clinicians (not expecting them to write RRULE strings):
┌─────────────────────────────────────────┐
│ ความถี่ (Frequency) │
│ ○ ทุกวัน (Daily) │
│ ● ทุกสัปดาห์ (Weekly) │
│ ○ ทุกเดือน (Monthly) │
│ ○ กำหนดเอง (Custom) │
│ │
│ วัน (Days): □จ ☑อ □พ □พฤ □ศ □ส □อา │
│ │
│ สิ้นสุด (Ends): │
│ ○ ไม่มีวันสิ้นสุด (Never) │
│ ● หลังจาก [8] ครั้ง (After N times) │
│ ○ วันที่ [____] (On date) │
│ │
│ สรุป: ทุกวันอังคาร, 8 ครั้ง │
│ RRULE: FREQ=WEEKLY;BYDAY=TU;COUNT=8 │
└─────────────────────────────────────────┘
8. Implementation Phases
Phase 1 — Core (ship in ~2 days)
- [ ] Supabase migration:
standing_orderstable + triggers + RLS - [ ] Frontend:
useStandingOrders(patientId)hook (Supabase query + RRULE eval) - [ ] Frontend: Wire into screening modal ORDER tab (replace mock data)
- [ ] Frontend: “Activate” button →
activateStandingOrders()→ existing API - [ ] Seed: 2-3 demo standing orders for test patients
Phase 2 — Creation UI (~1 day)
- [ ] RRULE picker component
- [ ] “Save as Standing Order” toggle in order dialog
- [ ] Direct insert to
standing_ordersfrom order dialog
Phase 3 — Signing & Review (~1 day)
- [ ] Co-sign badge + inbox integration
- [ ] Review status computation (trigger already handles it)
- [ ] Review/renew action in patient profile
- [ ] “Overdue” block on activation
Phase 4 — Management (~1 day)
- [ ] Patient profile standing orders panel
- [ ] Admin review dashboard
- [ ] Pause/cancel/edit actions
- [ ] Standing order history (activation_log display)
Phase 5 — Auto-Activation (future, only if needed)
- [ ] Edge function: on
ENCOUNTER_CHECKED_INevent → query matching standing orders → call REST API - [ ] Only for
activation_mode = 'auto'orders (ICU, chemo protocols)
9. Demo Seed Data
-- Patient: test patient on PH demo
insert into standing_orders (
patient_id, patient_hn, patient_name,
order_type, order_summary, priority, category,
order_template,
rrule, dtstart, max_occurrences,
activation_mode,
created_by, created_by_name, created_by_license, signed_at,
indication, instructions
) values (
'6406d4a16c8ff70012345678', 'HN-000001', 'นายทดสอบ ระบบ / Test Patient',
'lab', 'CBC + Electrolytes',
'ROUTINE', 'OUTPATIENT',
'[
{"productid": "lab-cbc-001", "product_name": "Complete Blood Count", "qty": 1, "priority": "ROUTINE", "specimen": "Blood", "container": "EDTA Tube"},
{"productid": "lab-electrolyte-001", "product_name": "Electrolyte Panel (Na/K/Cl/CO2)", "qty": 1, "priority": "ROUTINE", "specimen": "Blood", "container": "SST Tube"}
]'::jsonb,
'FREQ=WEEKLY;BYDAY=MO;COUNT=8',
now(),
8,
'confirm',
'sa', 'Dr. Admin', 'ว.00001', now(),
'Chronic kidney disease monitoring',
'เจาะเลือดก่อนอาหารเช้า (Fasting required)'
),
(
'6406d4a16c8ff70012345678', 'HN-000001', 'นายทดสอบ ระบบ / Test Patient',
'medication', 'Metformin 500mg BID',
'ROUTINE', 'OUTPATIENT',
'[
{"productid": "drug-metformin-500", "product_name": "Metformin 500mg", "qty": 60, "priority": "ROUTINE", "dosage": 1, "dosageuom": "tab", "frequency": "BID", "administrationUsage": {"name": "วันละ 2 ครั้ง เช้า-เย็น", "rruleString": "FREQ=DAILY;BYHOUR=8,18;BYMINUTE=0"}}
]'::jsonb,
'FREQ=MONTHLY;BYMONTHDAY=1',
now(),
null,
'confirm',
'sa', 'Dr. Admin', 'ว.00001', now(),
'Type 2 DM — monthly refill',
'ตรวจ HbA1c ทุก 3 เดือน'
);
10. Key Decisions Log
| Decision | Choice | Rationale |
|---|---|---|
| Storage | Supabase (not MongoDB) | Standing order is a “clinical intent” not an active order; activation calls existing backend API which runs all safety checks |
| RRULE evaluation | Client-side (rrule npm lib) | Already installed; avoids new backend endpoint; trivial computation |
| Signing model | Creator signs at creation + optional co-sign | Matches Thai Medical Council + Joint Commission standing order policies |
| Review cycle | 90-day default, configurable | Standard regulatory requirement; trigger auto-computes status |
| Activation | Nurse confirms by default | Safety: patient condition may have changed; auto mode reserved for ICU/chemo |
| Idempotency | last_activated_encounter_id check |
Prevents double-activation if nurse refreshes or re-opens screening modal |
| Order authorship | Original ordering physician (not activating nurse) | The order is “by” the doctor; nurse is the “verifier” — recorded in activation_log |
| Allergy/interaction checks | At activation time (existing pipeline) | Patient allergies may change between standing order creation and activation |
| Template shape | Matches CreateOrderRequestDto.listitems[] |
Zero transformation needed at activation — direct passthrough |
11. Regulatory Compliance Notes
| Requirement | How We Address It |
|---|---|
| Standing orders must be signed by licensed physician | created_by_license + signed_at fields |
| Periodic review required (TJC, Thai MC) | review_interval_days + auto-computed review_status + blocks activation when overdue |
| Must document clinical indication | indication + icd10_codes fields |
| Nurse must verify clinical appropriateness before activation | activation_mode = 'confirm' (default); activation_log records who verified |
| Co-signature for trainees | co_sign_required + co_signed_at + inbox workflow |
| Audit trail | activation_log jsonb array + Supabase row-level audit via updated_at |
| Ability to discontinue at any time | status = 'cancelled' with required status_reason |
| Patient-specific (not blanket protocols) | Keyed on patient_id; institution-wide protocols remain in OrderSet |
12. Relationship to Existing Features
┌─────────────────────────────────────────────────────────────┐
│ ORDER ECOSYSTEM │
├─────────────────────────────────────────────────────────────┤
│ │
│ OrderSet (MongoDB) standing_orders (Supabase) │
│ ───────────────── ────────────────────────── │
│ Institution-wide Patient-specific │
│ "Cardiac workup panel" "CBC every Monday for Mr. X" │
│ Doctor picks from catalog Auto-surfaces at check-in │
│ No recurrence RRULE-driven │
│ No signing Signed + co-signed │
│ Template only Template + activation log │
│ │
│ Both feed into → createOrderRequestApi() │
│ Both result in → normal OrderRequest + pipeline │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ OrderRequest.refill[] (MongoDB) │
│ ─────────────────────────────── │
│ Single future date ("come back May 19") │
│ Tied to appointment │
│ No recurrence │
│ DEPRECATED by standing_orders (superset) │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ AdministrationUsage.rruleString (MongoDB) │
│ ──────────────────────────────────────── │
│ WITHIN-ORDER timing ("take pill at 8am, 12pm, 6pm, 10pm")│
│ NOT cross-encounter ("create order every Monday") │
│ DIFFERENT concept — lives inside an active order │
│ │
└─────────────────────────────────────────────────────────────┘
13. File Plan
| File | Purpose |
|---|---|
web/supabase/migrations/20260517_standing_orders.sql |
Table + triggers + RLS + seed |
web/src/services/medbase/standing-orders.service.ts |
Supabase CRUD (select, insert, update) |
web/src/hooks/useStandingOrders.ts |
Query + RRULE evaluation hook |
web/packages/medical-kit/src/order/components/standing-order/RRulePicker.tsx |
RRULE builder UI |
web/packages/medical-kit/src/order/components/standing-order/StandingOrdersTab.tsx |
Screening modal tab |
web/packages/medical-kit/src/order/components/standing-order/ActivateDialog.tsx |
Confirmation + activate |
web/packages/medical-kit/src/order/components/standing-order/StandingOrderPanel.tsx |
Patient profile view |
web/packages/medical-kit/src/order/components/standing-order/interface.ts |
Types |
web/packages/admin-kit/src/standing-orders/StandingOrderReviewDashboard.tsx |
Admin review page |