Skip to main content
Glama
TaskInputNote.tsx7.66 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { ActionIcon, Button, Card, Divider, Flex, Modal, Paper, ScrollArea, Stack, Text, Textarea, } from '@mantine/core'; import { createReference, formatDate, getDisplayString, getReferenceString } from '@medplum/core'; import type { Annotation, QuestionnaireResponse, Task, Reference } from '@medplum/fhirtypes'; import { Loading, useMedplum, useMedplumProfile, useResource } from '@medplum/react'; import { IconCheck, IconTrash } from '@tabler/icons-react'; import React, { useState } from 'react'; import { showErrorNotification } from '../../utils/notifications'; import { TaskQuestionnaireForm } from './encounter/TaskQuestionnaireForm'; import { useDebouncedUpdateResource } from '../../hooks/useDebouncedUpdateResource'; import { TaskNoteItem } from './TaskNoteItem'; import { useDebouncedCallback } from '@mantine/hooks'; import { SAVE_TIMEOUT_MS } from '../../config/constants'; interface TaskInputNoteProps { task: Task | Reference<Task>; allowEdit?: boolean; onTaskChange?: (task: Task) => void; onDeleteTask?: (task: Task) => void; } export function TaskInputNote(props: TaskInputNoteProps): React.JSX.Element { const { task: initialTask, allowEdit = true, onTaskChange, onDeleteTask } = props; const medplum = useMedplum(); const debouncedUpdateResource = useDebouncedUpdateResource(medplum); const author = useMedplumProfile(); const task = useResource(initialTask); const [note, setNote] = useState<string>(''); const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false); const handleAddComment = async (): Promise<void> => { if (!task) { return; } const comment: Annotation = { text: note, authorReference: author && createReference(author), time: new Date().toISOString(), }; const taskNotes = [...(task.note || []), comment]; try { const updatedTask = { ...task, note: taskNotes, } as Task; onTaskChange?.(updatedTask); setNote(''); } catch (error) { showErrorNotification(error); } }; const handleDeleteTask = (): void => { setShowDeleteModal(true); }; const confirmDeleteTask = async (): Promise<void> => { if (!task) { return; } onDeleteTask?.(task); setShowDeleteModal(false); }; const handleMarkAsCompleted = async (): Promise<void> => { if (!task) { return; } try { const result: Task = { ...task, status: 'completed', }; onTaskChange?.(result); await debouncedUpdateResource(result); } catch (error) { showErrorNotification(error); } }; const saveQuestionnaireResponse = useDebouncedCallback( async (task: Task, response: QuestionnaireResponse): Promise<void> => { try { if (response.id) { await medplum.updateResource<QuestionnaireResponse>(response); } else { const updatedResponse = await medplum.createResource<QuestionnaireResponse>(response); const updatedTask = await medplum.updateResource<Task>({ ...task, output: [ { type: { text: 'QuestionnaireResponse' }, valueReference: { reference: getReferenceString(updatedResponse) }, }, ], }); onTaskChange?.(updatedTask); } } catch (err) { showErrorNotification(err); } }, SAVE_TIMEOUT_MS ); if (!task) { return <Loading />; } return ( <Flex direction="column" h="100%"> <Paper h="100%"> <Flex justify="space-between" p="lg" h={72}> <Flex justify="left" align="center" direction="row" pr="md"> <Text size="xl" fw={600} lh={1.2}> {task.code?.text ?? `Task`} {task?.authoredOn && ` from ${formatDate(task?.authoredOn)}`} </Text> </Flex> {allowEdit && ( <Flex align="center" gap="md"> {onDeleteTask && ( <ActionIcon variant="outline" c="dimmed" color="gray" aria-label="Delete Task" radius="xl" w={36} h={36} onClick={() => handleDeleteTask()} > <IconTrash size={24} /> </ActionIcon> )} <ActionIcon variant={task.status === 'completed' ? 'filled' : 'outline'} color={task.status === 'completed' ? 'blue' : 'gray'} aria-label="Mark as Completed" radius="xl" w={36} h={36} onClick={() => handleMarkAsCompleted()} > <IconCheck size={24} /> </ActionIcon> </Flex> )} </Flex> <ScrollArea w="100%" h="calc(100% - 70px)" p="lg"> {task.description && ( <Stack mb="lg"> <Text size="lg">{task.description}</Text> <Divider /> </Stack> )} <Stack> {task?.focus?.reference?.startsWith('Questionnaire/') && ( <> <Stack gap={0}> <Text size="lg" fw={600} mb="lg"> Related Questionnaire </Text> <Card withBorder shadow="sm" p="md"> <TaskQuestionnaireForm key={task.focus.reference} task={task} onChangeResponse={(response) => saveQuestionnaireResponse(task, response)} /> </Card> </Stack> <Divider /> </> )} <Stack gap={0}> <Text size="lg" fw={600} mb="md"> Notes </Text> {task.note?.map((note, index) => ( <TaskNoteItem key={note.id || index} note={note} index={index} /> ))} {allowEdit && ( <Stack gap="xs"> <Textarea placeholder="Add a note..." minRows={4} value={note ?? ''} onChange={(e) => setNote(e.currentTarget.value)} autosize /> <Flex justify="flex-end"> <Button type="submit" disabled={!note || note.trim() === ''} onClick={handleAddComment}> Submit </Button> </Flex> </Stack> )} </Stack> </Stack> </ScrollArea> <Modal opened={showDeleteModal} onClose={() => setShowDeleteModal(false)} title="Delete Task" size="md" centered > <Stack gap="md"> <Text>Are you sure you want to delete this task? This action cannot be undone.</Text> <Text fw={500} c="dimmed"> Task: {getDisplayString(task)} </Text> <Flex justify="flex-end" gap="sm"> <Button variant="outline" onClick={() => setShowDeleteModal(false)}> Cancel </Button> <Button color="red" onClick={confirmDeleteTask}> Delete </Button> </Flex> </Stack> </Modal> </Paper> </Flex> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server