Skip to content

feat(calendar): Replace PostCalendar visual layer with an OpenEMR-owned rendering layer (day/resource first) #12390

@sjpadgett

Description

@sjpadgett

Summary

Replace the legacy PostNuke PostCalendar visual layer (bundled Smarty 2.3.1, pn* shim,
SQL-in-.html templates) with a modern, OpenEMR-owned calendar rendering layer fed by normalized
JSON endpoints. The appointment data model and write workflows are explicitly preserved in this
issue. Initial target is the provider/resource day view; month/week follow once the day/resource
workflow is stable.

This is the rendering-only first slice of the broader calendar modernization (see the modernization
RFC). It deliberately does not change the schema, recurrence storage, status codes, the FHIR
mapping, or any write path.

Why rendering-only, data-model-preserved

A working-tree sweep shows openemr_postcalendar_events is written from ~30+ sites across the
codebase, including:

  • encounter↔appointment linkage — library/encounter_events.inc.php
  • reminders / recall — library/MedEx/API.php, modules/sms_email_reminder/*, interface/batchcom/*
  • holidays — src/Services/HolidayService.php, interface/main/holidays/Holidays_Storage.php
  • telehealth — oe-module-comlink-telehealth · fax/SMS — oe-module-faxsms
  • CCDA import — src/Services/Cda/CdaTemplateImportDispose.php
  • tracker writing status/room back to events — src/Services/PatientTrackerService.php
  • the entire patient portal schedulerportal/add_edit_event_user.php, portal/find_appt_popup_user.php
  • the canonical service — src/Services/AppointmentService.php

Changing the schema would require touching all of them. Replacing only the rendering touches none
of them: every writer above keeps working unchanged because the tables don't change.

Scope

In scope

  • New OpenEMR-owned day / resource (provider-column) calendar rendering.
  • Read-only normalized JSON endpoint(s) exposing existing appointment + availability data.
  • Threading the new view into existing launch points behind a feature flag, with the legacy
    calendar remaining the default until proven.
  • Reusing the existing add/edit dialog (add_edit_event.php) as the editor — no write-path change.

Out of scope (explicit)

  • Any schema change; recurrence (pc_recurrspec) stays serialized; status (pc_apptstatus) stays as-is.
  • FHIR remodel / RRULE / UTC migration (covered by the RFC's later phases).
  • Write-path refactor; tracker/portal/reminder writers; the patient portal UI.
  • Month and week views (fast-follow issues once day/resource is stable).

Architecture of the change

Read — normalized JSON endpoint(s)

  • Add a read method to src/Services/AppointmentService.php (reuse its existing event SELECT; do not
    duplicate SQL) and expose it via a thin controller:
    • GET /calendar/events — appointments + availability blocks for a date + facility + provider set.
    • GET /calendar/resources — the provider/room list that forms the day-view columns.
  • This replaces the read leaks identified in Phase 0 (views/{day,week,month}/ajax_template.html,
    find_appt_popup.php) for the new view — the new renderer never touches the DB directly.
  • The endpoint normalizes the split pc_eventDate + pc_startTime/pc_endTime into ISO
    datetime strings for the client. It does not change storage and does not fix timezone
    semantics — tz correctness remains a known follow-up.

Render — OpenEMR-owned, no premium/commercial deps

  • Build the day/resource grid as OpenEMR-owned rendering (time rows × provider columns; absolutely
    positioned event blocks; availability shading from the in_office/out_of_office categories).
  • Do not adopt a full calendar framework for this view. The provider/resource-column view is the
    feature that is paywalled across the ecosystem: FullCalendar resource/timeline is Premium;
    Schedule-X resource view is Premium (and v4 moved drag/resize to Premium); MUI X resource layouts
    are MIT but alpha and pull in all of MUI. Owning the grid avoids the licensing trap entirely and
    matches OpenEMR's dependency-minimization posture.
  • Permissive primitives are acceptable where they reduce risk: e.g. interact.js (MIT) for
    drag/resize hit-handling, and a small date utility. No GPLv3-keyed or commercial scheduler bundles.

Write / edit — unchanged

  • Clicking an empty slot or an event opens the existing add_edit_event.php dialog (same
    workflow, same validation, same writers). The new grid only replaces the canvas around it, then
    refreshes from the JSON endpoint on save. No appointment write logic is reimplemented here.

Threading / rollout

  • Gate the new view behind a feature flag via ContextManifest::featureEnabled(); legacy calendar
    stays the default. Light it up first where the day/resource workflow matters most.
  • Route the top-nav Calendar entry and the patient-summary "Add appointment" launch to the new view
    when the flag is on, using the existing ScriptFilterEvent/StyleFilterEvent/menu hooks. The flag
    is the rollback switch.

JSON contract (read-only, this issue)

GET /calendar/events?date=YYYY-MM-DD&facility=<id>&providers=<id,id>&category=<id>&status=<code>

{
  "date": "2026-06-03",
  "facility": 3,
  "resources": [                       // also available via /calendar/resources
    { "id": "1", "name": "Dr. Smith", "type": "provider", "color": "#2b6" }
  ],
  "events": [
    {
      "eid": 1421,
      "uuid": "9f2c…",                 // reuse existing binary(16), hex-encoded
      "resourceId": "1",               // pc_aid
      "pid": "1207",
      "patientName": "Doe, Jane",
      "title": "Office Visit",
      "catId": 9,
      "categoryColor": "#cce5ff",
      "start": "2026-06-03T09:00:00",  // normalized from pc_eventDate + pc_startTime (local, as-stored)
      "end":   "2026-06-03T09:30:00",
      "status": "-",                   // raw pc_apptstatus; mapping is a follow-up
      "room": "",
      "allDay": false,
      "kind": "appointment",           // or "availability" for in_office/out_of_office
      "recurring": true               // surfaced from pc_recurrtype; expansion stays server-side
    }
  ]
}

Tasks

  • Add read method(s) to AppointmentService reusing the existing event query (events + resources).
  • Add controller + routes for /calendar/events and /calendar/resources (read-only, ACL-checked).
  • Define and document the JSON shape above; add a service test + an endpoint fixture.
  • Build the OpenEMR-owned day/resource grid (provider columns, time rows, availability shading).
  • Wire event/slot click → existing add_edit_event.php dialog; refresh grid from endpoint on save.
  • Feature-flag the view via ContextManifest; route nav + add-appointment launch points behind it.
  • Verify the legacy calendar remains the default and the flag cleanly toggles back.

Acceptance criteria

  • The day/resource view renders appointments and availability for a chosen date/facility/provider set
    entirely from the JSON endpoint — no direct DB access from the view, no Smarty, no pn*.
  • Creating/editing/cancelling from the new view goes through the existing dialog and writers; data
    written is identical to the legacy calendar (same rows, same columns).
  • Every existing writer (encounter linkage, MedEx/reminders, holidays, telehealth, faxsms, CCDA
    import, tracker, portal) continues to function with zero changes.
  • The new view is reachable only behind the feature flag; toggling it off restores the legacy
    calendar with no data migration.

Out of scope / follow-ups

  • Month and week views (separate issues, after day/resource is stable).
  • Timezone normalization, status→FHIR value-set mapping, recurrence→RRULE, schema normalization
    (RFC later phases).
  • Patient portal rendering (portal/*) — separate track.
  • Write-path consolidation into AppointmentService (RFC Phase 2).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions