Pathology Integration Guide
How to wire the pathology workflow editor into the running app: footer, modal renderer, aftersave, route, policy seeding.
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.
2. Footer Mount Point
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.tsxweb/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:
- Reads the active pathology workflow template
- Computes the legal next states for the current specimen
- Calls
usePolicyGate({ trigger: 'pathology_<next_state>' })for each candidate - 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:
- 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() }),
}));
-
From the dialog’s
onAfterCollect, callbumpTransition(). -
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.tsshould emitpathology.state_changedon every transition. Currently emits onlylabRequest.updated. - Encounter-orchestrator handler: Add
handlePathologyStateChangetoinfrastructure/medbase/functions/encounter-orchestrator/index.ts. Project pathology state intoencounter_journey_cache.pending_tickets(keypathology) anddepartment_queues(dept_type: 'pathology'). BlockEnumpromotion: Once the demo lands, movePathologyNodeTypevalues intomain-flow-editor/components/workflow/types.tsBlockEnumso they’re first-class in the visual graph editor too.NodeConfigDrawer.extraTabsupstream: Add theextraTabs?: ConfigDrawerTab[]prop toNodeConfigDrawer.tsx, then delete the localPathologyConfigDrawer.tsxshim.