Skip to content

Commit

Permalink
Add patient notes card
Browse files Browse the repository at this point in the history
  • Loading branch information
atuonufure committed Aug 26, 2024
1 parent 312ee19 commit f6f4f75
Show file tree
Hide file tree
Showing 17 changed files with 2,298 additions and 17 deletions.
4 changes: 2 additions & 2 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ module.exports = {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
"string-to-lingui/t-call-in-function": 2,
'string-to-lingui/t-call-in-function': 2,
'import/no-unresolved': [
2,
{
ignore: ['fhir/r4b', '@beda.software/emr-config', '@beda.software/aidbox-types'], // Fixes error: Unable to resolve path to module 'fhir/r4b'.
ignore: ['fhir/r4b', '@beda.software/emr-config', '@beda.software/aidbox-types', '@mdxeditor/editor'], // Fixes error: Unable to resolve path to module 'fhir/r4b'.
},
],
'import/order': [
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@jitsi/react-sdk": "^1.3.0",
"@lingui/core": "^4.3.0",
"@lingui/react": "^4.3.0",
"@mdxeditor/editor": "^3.11.0",
"@sentry/browser": "^7.57.0",
"aidbox-react": "^1.10.0",
"antd": "5.6.4",
Expand Down Expand Up @@ -127,7 +128,7 @@
"vite-plugin-externalize-deps": "^0.8.0",
"vite-plugin-turbosnap": "^1.0.2",
"vitest": "^0.33.0"
},
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"files": [
Expand Down
36 changes: 36 additions & 0 deletions resources/seeds/Mapping/patient-note-create-extract.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
body:
$let:
noteTitle: $ fhirpath("QuestionnaireResponse.repeat(item).where(linkId='note-title').answer.valueString").0
noteContent: $ fhirpath("QuestionnaireResponse.repeat(item).where(linkId='note-content').answer.valueString").0
patientId: $ fhirpath("QuestionnaireResponse.repeat(item).where(linkId='patient-id').answer.valueString").0
performerId: $ fhirpath("QuestionnaireResponse.repeat(item).where(linkId='performer-id').answer.valueString").0
performerName: $ fhirpath("QuestionnaireResponse.repeat(item).where(linkId='performer-name').answer.valueString").0
$body:
type: transaction
entry:
- request:
url: /Observation
method: POST
fullUrl: urn:uuid:observation-id
resource:
category:
- coding:
- code: note
status: final
code:
coding:
- display: $ noteTitle
subject:
id: $ patientId
resourceType: Patient
performer:
- id: $ practitionerId
display: $ performerName
resourceType: Practitioner
text:
div: $ noteContent
status: additional
resourceType: Observation
resourceType: Bundle
id: patient-note-create-extract
resourceType: Mapping
57 changes: 57 additions & 0 deletions resources/seeds/Questionnaire/patient-note-create.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
id: patient-note-create
resourceType: Questionnaire
name: patient-note-create
title: patient note create
status: active
launchContext:
- name:
code: Patient
type:
- Patient
- name:
code: Author
type:
- Practitioner
mapping:
- id: patient-note-create-extract
resourceType: Mapping
item:
- text: Title
required: true
type: string
linkId: note-title
- text: Note
required: true
type: string
linkId: note-content
itemControl:
coding:
- code: markdown-content
- text: Patient ID
required: true
type: string
linkId: patient-id
hidden: true
initialExpression:
language: text/fhirpath
expression: "%Patient.id"
- text: Performer ID
required: true
type: string
linkId: performer-id
hidden: true
initialExpression:
language: text/fhirpath
expression: "%Author.id"
- text: Performer Name
required: true
type: string
linkId: performer-name
hidden: true
initialExpression:
language: text/fhirpath
expression: "%Author.name.first().given.first() + ' ' + %Author.name.first().family"
meta:
profile:
- https://beda.software/beda-emr-questionnaire
url: https://aidbox.emr.beda.software/ui/console#/entities/Questionnaire/patient-note-create
33 changes: 33 additions & 0 deletions resources/seeds/Questionnaire/patient-note-open.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
id: patient-note-open
resourceType: Questionnaire
name: patient-note-open
title: patient note open
status: active
launchContext:
- name:
code: Note
type:
- Observation
item:
- type: string
linkId: note-title
readOnly: true
initialExpression:
language: text/fhirpath
expression: "%Note.code.coding.first().display"
itemControl:
coding:
- code: markdown-title
- type: string
linkId: note-content
readOnly: true
initialExpression:
language: text/fhirpath
expression: "%Note.text.`div`"
itemControl:
coding:
- code: markdown-content
meta:
profile:
- https://beda.software/beda-emr-questionnaire
url: https://aidbox.emr.beda.software/ui/console#/entities/Questionnaire/patient-note-open
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
CodeToggle,
// CreateLink,
// linkDialogPlugin,
ListsToggle,
markdownShortcutPlugin,
MDXEditor,
headingsPlugin,
listsPlugin,
quotePlugin,
linkPlugin,
toolbarPlugin,
UndoRedo,
BoldItalicUnderlineToggles,
MDXEditorMethods,
} from '@mdxeditor/editor';
import { useEffect, useRef } from 'react';
import { useTheme } from 'styled-components';

import '@mdxeditor/editor/style.css';

interface MarkDownEditorProps {
markdownString: string;
onChange: (markdown: string) => void;
readOnly?: boolean;
}

export function MarkDownEditor({ markdownString = '', readOnly = false, onChange }: MarkDownEditorProps) {
const mdxEditorRef = useRef<MDXEditorMethods>(null);

useEffect(() => {
if (mdxEditorRef.current && markdownString !== mdxEditorRef.current.getMarkdown()) {
mdxEditorRef.current.setMarkdown(markdownString);
}
}, [markdownString]);

const theme = useTheme();

const plugins = [
headingsPlugin(),
listsPlugin(),
quotePlugin(),
linkPlugin(),
// TODO linkDialogPlugin({}),
markdownShortcutPlugin(),
];

if (!readOnly) {
plugins.push(
toolbarPlugin({
toolbarContents: () => {
return (
<div className="MarkDownToolBar" style={{ display: 'flex' }}>
<UndoRedo />
<Separator />
<BoldItalicUnderlineToggles />
<CodeToggle />
<Separator />
<ListsToggle />
{/* <Separator /> */}
{/* TODO <CreateLink /> */}
</div>
);
},
}),
);
}

return (
<MDXEditor
className={theme.mode === 'dark' ? 'dark-theme' : ''}
ref={mdxEditorRef}
readOnly={readOnly}
markdown={markdownString}
onChange={onChange}
contentEditableClassName="MarkDownEditorContent"
plugins={plugins}
/>
);
}

function Separator() {
return <div data-orientation="vertical" aria-orientation="vertical" role="separator"></div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { QuestionItemProps } from 'sdc-qrf';

import { useFieldController } from 'src/components/BaseQuestionnaireResponseForm/hooks';

import { MarkDownEditor } from './MarkDownEditor';

export function MDEditorControl({ parentPath, questionItem }: QuestionItemProps) {
const { linkId } = questionItem;
const fieldName = [...parentPath, linkId, 0, 'value', 'string'];
const { value, onChange } = useFieldController(fieldName, questionItem);

return <MarkDownEditor markdownString={value} onChange={onChange} readOnly={questionItem.readOnly} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import classNames from 'classnames';
import { QuestionItemProps } from 'sdc-qrf';

import { useFieldController } from 'src/components/BaseQuestionnaireResponseForm/hooks';
import s from 'src/components/BaseQuestionnaireResponseForm/readonly-widgets/ReadonlyWidgets.module.scss';
import { S } from 'src/components/BaseQuestionnaireResponseForm/readonly-widgets/ReadonlyWidgets.styles';

import { STitle } from './styles';

export function MDTitleDisplayControl({ parentPath, questionItem }: QuestionItemProps) {
const { linkId, hidden } = questionItem;
const fieldName = [...parentPath, linkId, 0, 'value', 'string'];
const { value } = useFieldController(fieldName, questionItem);

if (hidden) {
return null;
}

return (
<S.Question className={classNames(s.question, s.column, 'form__question')}>
<span className={s.questionText}>{value}</span>
<STitle.Divider />
</S.Question>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Divider } from 'antd';
import styled from 'styled-components';

export const STitle = {
Divider: styled(Divider)`
margin: 0;
`,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { t, Trans } from '@lingui/macro';
import { Button, notification } from 'antd';
import { Patient, Practitioner } from 'fhir/r4b';

import { WithId } from '@beda.software/fhir-react';

import { MDEditorControl } from 'src/components/BaseQuestionnaireResponseForm/widgets/MDEditorControl';
import { ModalTrigger } from 'src/components/ModalTrigger';
import { QuestionnaireResponseForm } from 'src/components/QuestionnaireResponseForm';
import { questionnaireIdLoader } from 'src/hooks/questionnaire-response-form-data';
import { selectCurrentUserRoleResource } from 'src/utils/role';

interface ModalNoteCreateProps {
patient: Patient;
onCreate: () => void;
}

export const ModalNoteCreate = (props: ModalNoteCreateProps) => {
const author = selectCurrentUserRoleResource() as WithId<Practitioner>;

return (
<ModalTrigger
title={t`Add Note`}
trigger={
<Button type="primary">
<span>
<Trans>Add note</Trans>
</span>
</Button>
}
>
{({ closeModal }) => (
<QuestionnaireResponseForm
questionnaireLoader={questionnaireIdLoader('patient-note-create')}
launchContextParameters={[
{ name: 'Patient', resource: props.patient },
{ name: 'Author', resource: author },
]}
itemControlQuestionItemComponents={{
'markdown-content': (props) => <MDEditorControl {...props} />,
}}
onSuccess={() => {
closeModal();
notification.success({ message: t`Note successfully created` });
props.onCreate();
}}
onCancel={closeModal}
/>
)}
</ModalTrigger>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Trans } from '@lingui/macro';
import { Button } from 'antd';
import { FhirResource, Observation } from 'fhir/r4b';

import { MDEditorControl } from 'src/components/BaseQuestionnaireResponseForm/widgets/MDEditorControl';
import { MDTitleDisplayControl } from 'src/components/BaseQuestionnaireResponseForm/widgets/MDTitleDisplayControl';
import { ModalTrigger } from 'src/components/ModalTrigger';
import { QuestionnaireResponseForm } from 'src/components/QuestionnaireResponseForm';
import { questionnaireIdLoader } from 'src/hooks/questionnaire-response-form-data';

import { S } from './styles';

interface ModalNoteOpenProps {
note: Observation;
onOpen: () => void;
}

export const ModalNoteOpen = (props: ModalNoteOpenProps) => {
return (
<ModalTrigger
title={null}
trigger={
<Button type="primary">
<span>
<Trans>Open</Trans>
</span>
</Button>
}
>
{({ closeModal }) => (
<QuestionnaireResponseForm
questionnaireLoader={questionnaireIdLoader('patient-note-open')}
launchContextParameters={[{ name: 'Note', resource: props.note as FhirResource }]}
itemControlQuestionItemComponents={{
'markdown-title': (props) => <MDTitleDisplayControl {...props} />,
'markdown-content': (props) => <MDEditorControl {...props} />,
}}
FormFooterComponent={() => {
return (
<S.Footer>
<Button type="primary" onClick={closeModal}>
<Trans>Close</Trans>
</Button>
</S.Footer>
);
}}
/>
)}
</ModalTrigger>
);
};
Loading

0 comments on commit f6f4f75

Please sign in to comment.