medOS ultra

Pathology Integration Guide

How to wire the pathology workflow editor into the running app: footer, modal renderer, aftersave, route, policy seeding.

4 min readUpdated 2026-05-01docs/pathology/integration-guide.md

How to wire the pathology workflow editor into the running app. Three integration points (footer, modal renderer, aftersave) plus admin route + policy seeding.

1. Admin Route

Add to web/src/routes.tsx:

import { PathologyWorkflowEditorPage } from '@/common/components/medical/builder/pathology-workflow-editor';

// inside admin route children:
{
  path: 'pathology-workflow',
  element: <PathologyWorkflowEditorPage />
}

Route URL: /admin/pathology-workflow

Optionally add a nav link in the admin sidebar config — this folder does not edit the sidebar to keep changes additive. Search for the existing /admin/worklist-config entry and add a sibling.

PathologyStateFooter is exported from pathology-workflow-editor/components/PathologyStateFooter. Mount it once per pathology container page. Recommended mount points:

  • web/src/containers/pathology/page.tsx (top-level pathology landing)
  • web/src/containers/pathology/laboratory/page.tsx
  • web/src/containers/pathology/collect-specimens/page.tsx
import { PathologyStateFooter } from '@/common/components/medical/builder/pathology-workflow-editor/components/PathologyStateFooter';

export default function PathologyLaboratoryPage() {
  return (
    <>
      {/* existing page content */}
      <PathologyStateFooter
        encounterId={selectedEncounterId}
        specimenId={selectedSpecimenId}
      />
    </>
  );
}

The footer is a fixed-position bar (position: sticky; bottom: 0) that:

  1. Reads the active pathology workflow template
  2. Computes the legal next states for the current specimen
  3. Calls usePolicyGate({ trigger: 'pathology_<next_state>' }) for each candidate
  4. Renders the primary transition button (or a blocked state with the policy gate message)

If specimenId is null (no specimen selected), the footer renders a placeholder (“Select a specimen to see actions”).

3. Modal Renderer Path

For transitions that need a form (Release, Amend, Reject), the footer button does not mutate directly. It dispatches:

import { openModal } from '@/store/modalSlice'; // existing
// or directly via the GlobalModalRenderer registry

dispatch(openModal({
  id: 'universalTransitionModal',
  payload: {
    trigger: 'pathology_release_report',
    fromState: 'sign_out',
    toState: 'released',
    specimenId,
    encounterId,
  },
}));

The wildcard handler in web/src/common/components/medical/builder/modal/registry/modalRegistry.ts:129 resolves universalTransitionModal to a single component that renders any transition’s confirmation form. Pathology transitions reuse this without registering a new modal.

If a pathology transition needs a fully custom modal (e.g., the slide-prep block-list editor), register it in:

// web/src/common/components/medical/builder/modal/registry/modalRegistry.ts
PATHOLOGY_BLOCK_LIST_MODAL: PathologyBlockListModal,

…and dispatch openModal({ id: 'PATHOLOGY_BLOCK_LIST_MODAL', payload: { ... } }) from the footer.

4. Aftersave Hook Path

Existing pathology dialogs already accept an onAfterCollect callback (see web/packages/diagnostics-kit/src/pathology/collect-specimens/components/dialog/dialog-specimen-collect/DialogSpecimenCollect.tsx:67). To make the footer reactive to inline mutations:

  1. Create a tiny zustand store at pathology-workflow-editor/state/pathologyTransitionStore.ts:
import { create } from 'zustand';

interface PathologyTransitionStore {
  lastTransitionAt: number;
  bumpTransition: () => void;
}

export const usePathologyTransitionStore = create<PathologyTransitionStore>((set) => ({
  lastTransitionAt: 0,
  bumpTransition: () => set({ lastTransitionAt: Date.now() }),
}));
  1. From the dialog’s onAfterCollect, call bumpTransition().

  2. The footer subscribes:

const lastTransitionAt = usePathologyTransitionStore((s) => s.lastTransitionAt);

useEffect(() => {
  refetchSpecimenState();
}, [lastTransitionAt]);

This is the third surface — an inline action far from the footer still updates it within ~50ms (zustand sync), without a Supabase round-trip. The Supabase realtime channel still fires for cross-tab consistency.

5. Policy Gate Seeding

Run this SQL once per region (or via the “Generate Gate” buttons in the editor’s side panels) to seed sane defaults:

-- Block release without signature
INSERT INTO policy_gates (name, status, priority, trigger_action, scope_json, predicate_json, action_json)
VALUES (
  'Pathology release requires signature', 'active', 100, 'pathology_release_report',
  '{"node_types": ["pathology-release"]}',
  '{"actor.signature_id": null}',
  '{"effect": "block", "message": "Pathologist signature required"}'
);

-- Warn if grossing > 30 minutes
INSERT INTO policy_gates (name, status, priority, trigger_action, scope_json, predicate_json, action_json)
VALUES (
  'Long grossing warning', 'active', 50, 'pathology_complete_grossing',
  '{}',
  '{"specimen.in_grossing_minutes": {"$gt": 30}}',
  '{"effect": "warn", "message": "Grossing has exceeded 30 minutes"}'
);

-- Block amendment after 30 days
INSERT INTO policy_gates (name, status, priority, trigger_action, scope_json, predicate_json, action_json)
VALUES (
  'Amendment cooldown', 'active', 100, 'pathology_amend_report',
  '{}',
  '{"report.released_days_ago": {"$gt": 30}}',
  '{"effect": "block", "message": "Amendment requires director approval after 30 days"}'
);

6. Bilingualism

Per CLAUDE.md rule #7, every pathology node label needs translations. Add to:

web/public/locales/{ja,th,fil,en}/pathology.json

Keys mirror the PathologyNodeType values:

{
  "nodes": {
    "pathology-register": "Register",
    "pathology-grossing": "Gross Examination",
    "pathology-slide-prep": "Slide Preparation",
    "...": "..."
  }
}

The renderers consume them via useTranslation('pathology').

Deep Mock — Future Work

These tasks are out of scope for the demo mock but tracked here:

  • Backend Moleculer event emission: services/diagnostic/.../labRequest.service.ts should emit pathology.state_changed on every transition. Currently emits only labRequest.updated.
  • Encounter-orchestrator handler: Add handlePathologyStateChange to infrastructure/medbase/functions/encounter-orchestrator/index.ts. Project pathology state into encounter_journey_cache.pending_tickets (key pathology) and department_queues (dept_type: 'pathology').
  • BlockEnum promotion: Once the demo lands, move PathologyNodeType values into main-flow-editor/components/workflow/types.ts BlockEnum so they’re first-class in the visual graph editor too.
  • NodeConfigDrawer.extraTabs upstream: Add the extraTabs?: ConfigDrawerTab[] prop to NodeConfigDrawer.tsx, then delete the local PathologyConfigDrawer.tsx shim.
Ask Anything