medOS ultra

Standing Orders (RRULE)

RRULE-based recurring/future order templates that auto-surface at patient check-in.

21 min read diagramsUpdated 2026-05-17docs/architecture/standing-orders-rrule-system.md

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 AcknowledgementRequest system)
  • 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 in activation_log
  • The resulting OrderRequest has requester = 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_by
  • status_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:

  • PreOrderData currently shows dataMockFutureOrder — it’s the exact placeholder for this feature
  • Adding a separate STANDING_ORDERS tab 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.tsx stops importing mock → uses useStandingOrders(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):

  1. hospital-outpatient-screeninglist → click patient row → DialogScreening
  2. WorkflowConfigBasedTabs → dispatch DIALOG_SCREENING modal

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:

  1. Submit current + save standing → fires createOrderRequestApi() for today AND inserts standing_orders for future
  2. 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_orders table + 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_orders from 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_IN event → 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
Ask Anything