Skip to content

Commit

Permalink
Allow showing/editing/importing questionnaire in smart ehr
Browse files Browse the repository at this point in the history
  • Loading branch information
olimsaidov committed Oct 30, 2024
1 parent 73d7195 commit 727178d
Show file tree
Hide file tree
Showing 14 changed files with 324 additions and 77 deletions.
5 changes: 2 additions & 3 deletions aidbox-forms-smart-launch/src/components/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { SidebarInset, SidebarProvider } from "@/ui/sidebar.jsx";
import { Header } from "@/components/header.jsx";
import { Sidebar } from "@/components/sidebar.jsx";
import * as React from "react";
import { Suspense } from "react";
import { LaunchContextProvider } from "@/hooks/use-launch-context.jsx";
import { ClientProvider } from "@/hooks/use-client.jsx";
import { cn } from "@/lib/utils.js";
import { Outlet, useNavigation } from "react-router-dom";
import { Suspense } from "react";
import { Outlet } from "react-router-dom";
import { Loading } from "@/components/loading.jsx";

export const Page = () => {
Expand Down
20 changes: 14 additions & 6 deletions aidbox-forms-smart-launch/src/components/pagination.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@ import {
PaginationPrevious,
} from "@/ui/pagination.jsx";
import * as React from "react";
import { useSearchParams } from "react-router-dom";

export const Pagination = ({ currentPage, totalPages }) => {
const [searchParams] = useSearchParams();

const withPage = (page) => {
searchParams.set("page", page);
return `?${searchParams}`;
};

if (totalPages <= 1) {
return null;
}
Expand All @@ -22,13 +30,13 @@ export const Pagination = ({ currentPage, totalPages }) => {
<PaginationContent>
{paginationData.prevButtonEnabled && (
<PaginationItem>
<PaginationPrevious to={`?page=${currentPage - 1}`} />
<PaginationPrevious to={withPage(currentPage - 1)} />
</PaginationItem>
)}

{paginationData.showFirstPageButton && (
<PaginationItem>
<PaginationLink to={`?page=1`}>{1}</PaginationLink>
<PaginationLink to={withPage(1)}>{1}</PaginationLink>
</PaginationItem>
)}

Expand All @@ -40,7 +48,7 @@ export const Pagination = ({ currentPage, totalPages }) => {

{paginationData.pagesBeforeCurrent.map((page) => (
<PaginationItem key={page}>
<PaginationLink to={`?page=${page}`}>{page}</PaginationLink>
<PaginationLink to={withPage(page)}>{page}</PaginationLink>
</PaginationItem>
))}

Expand All @@ -50,7 +58,7 @@ export const Pagination = ({ currentPage, totalPages }) => {

{paginationData.pagesAfterCurrent.map((page) => (
<PaginationItem key={page}>
<PaginationLink to={`?page=${page}`}>{page}</PaginationLink>
<PaginationLink to={withPage(page)}>{page}</PaginationLink>
</PaginationItem>
))}

Expand All @@ -62,15 +70,15 @@ export const Pagination = ({ currentPage, totalPages }) => {

{paginationData.showLastPageButton && (
<PaginationItem>
<PaginationLink to={`?page=${totalPages}`}>
<PaginationLink to={withPage(totalPages)}>
{totalPages}
</PaginationLink>
</PaginationItem>
)}

{paginationData.nextButtonEnabled && (
<PaginationItem>
<PaginationNext to={`?page=${currentPage + 1}`} />
<PaginationNext to={withPage(currentPage + 1)} />
</PaginationItem>
)}
</PaginationContent>
Expand Down
2 changes: 0 additions & 2 deletions aidbox-forms-smart-launch/src/components/patient-card.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/ui/dropdown-menu.jsx";
import { Button } from "@/ui/button.jsx";
Expand Down
51 changes: 46 additions & 5 deletions aidbox-forms-smart-launch/src/components/questionnaire-builder.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,57 @@
import { useRef } from "react";
import { useQuery } from "@tanstack/react-query";
import { publicBuilderClient } from "@/hooks/use-client.jsx";
import { useEffect, useRef } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { publicBuilderClient, useClient } from "@/hooks/use-client.jsx";
import { useAwaiter } from "@/hooks/use-awaiter.jsx";
import { findQuestionnaireWithClient, saveQuestionnaire } from "@/lib/utils.js";
import { useToast } from "@/hooks/use-toast.js";

export const QuestionnaireBuilder = ({ id }) => {
const ref = useRef();
const client = useClient();
const { toast } = useToast();
const toastShown = useRef(false);

const { data: questionnaire } = useQuery({
const {
data: [usedClient, questionnaire],
} = useQuery({
queryKey: ["questionnaire", id],
queryFn: () => publicBuilderClient.request(`Questionnaire/${id}`),
queryFn: () => findQuestionnaireWithClient(client, id),
});

const mutation = useMutation({
mutationFn: (questionnaire) => saveQuestionnaire(usedClient, questionnaire),
onSuccess: () => {
if (!toastShown.current) {
toastShown.current = true;
toast({
title: "Questionnaire is autosaved",
description: "All changes are saved automatically",
});
}
},
});

useAwaiter(ref);

useEffect(() => {
if (usedClient !== publicBuilderClient) {
const current = ref.current;
const handler = (e) => mutation.mutate(e.detail);

current.addEventListener("change", handler);

return () => {
current.removeEventListener("change", handler);
};
} else {
toast({
title: "This questionnaire is read-only",
description:
"You can't save changes to questionnaires from the library. Please import it first to your EHR to make changes.",
});
}
}, []);

return (
<aidbox-form-builder
hide-back={true}
Expand All @@ -24,6 +63,8 @@ export const QuestionnaireBuilder = ({ id }) => {
hide-edit-theme={true}
hide-save-theme={true}
hide-convert={true}
hide-save={true}
disable-save={true}
ref={ref}
value={JSON.stringify(questionnaire)}
style={{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { publicBuilderClient } from "@/hooks/use-client.jsx";
import { useClient } from "@/hooks/use-client.jsx";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useRef } from "react";
import { useToast } from "@/hooks/use-toast.js";
import { useAwaiter } from "@/hooks/use-awaiter.jsx";
import { findQuestionnaire } from "@/lib/utils.js";

export const QuestionnairePreview = ({ id }) => {
const ref = useRef();
const client = useClient();

const { data: questionnaire } = useQuery({
queryKey: ["questionnaire", id],
queryFn: () => publicBuilderClient.request(`Questionnaire/${id}`),
queryFn: () => findQuestionnaire(client, id),
});

const { toast } = useToast();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ import {
DropdownMenuTrigger,
} from "@/ui/dropdown-menu.jsx";
import { Button } from "@/ui/button.jsx";
import { Avatar, AvatarFallback, AvatarImage } from "@/ui/avatar.jsx";
import { ChevronDown, Cog, LogOut, Settings2, UserPen } from "lucide-react";
import { ChevronDown, LogOut, Settings2, UserPen } from "lucide-react";
import * as React from "react";
import { UserAvatar } from "@/components/user-avatar.jsx";
import { useLaunchContext } from "@/hooks/use-launch-context.jsx";
import { constructName } from "@/lib/utils.js";
import { useClient } from "@/hooks/use-client.jsx";
import { useHref, useNavigate } from "react-router-dom";
import { useHref } from "react-router-dom";

export const UserDropdownMenu = () => {
const { user } = useLaunchContext();
Expand Down
2 changes: 2 additions & 0 deletions aidbox-forms-smart-launch/src/hooks/use-client.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const scope = [
"launch/questionnaire", // Request Questionnaire to be included in the launch context
"launch/questionnaireresponse", // Request QuestionnaireResponse to be included in the launch context

"user/Questionnaire.crus",

"patient/Patient.r", // Request read access to Patient resource
"patient/QuestionnaireResponse.crus", // Request create, read, update access to QuestionnaireResponse resource
].join(" ");
Expand Down
4 changes: 2 additions & 2 deletions aidbox-forms-smart-launch/src/hooks/use-launch-context.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createContext, useContext, useEffect, useState } from "react";
import { useClient, publicBuilderClient } from "@/hooks/use-client.jsx";
import { createContext, useContext } from "react";
import { publicBuilderClient, useClient } from "@/hooks/use-client.jsx";
import { useQuery } from "@tanstack/react-query";

const readLaunchResource = async (client, resourceType) => {
Expand Down
48 changes: 43 additions & 5 deletions aidbox-forms-smart-launch/src/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@ export function saveQuestionnaireResponse(
body: JSON.stringify({
...questionnaireResponse,
// Plugging questionnaire.id in because SMART Health IT requires QRs to have Questionnaire/{id} as reference
questionnaire: `Questionnaire/${questionnaire.id}`,
questionnaire: questionnaire.url
? questionnaire.url
: `Questionnaire/${questionnaire.id}`,
meta: {
...questionnaireResponse.meta,
source: "https://aidbox.github.io/examples/aidbox-forms-smart-launch",
Expand All @@ -174,6 +176,32 @@ export function saveQuestionnaireResponse(
});
}

export function saveQuestionnaire(client, questionnaire) {
console.log({ client });

let url = "Questionnaire";
let method = "POST";

if (questionnaire.id) {
url += `/${questionnaire.id}`;
method = "PUT";
}

return client.request({
url,
method,
headers: { "Content-Type": "application/fhir+json" },
body: JSON.stringify(questionnaire),
});
}

export function deleteQuestionnaire(client, questionnaire) {
return client.request({
url: `Questionnaire/${questionnaire.id}`,
method: "DELETE",
});
}

export async function createQuestionnaireResponse({
client,
questionnaire,
Expand Down Expand Up @@ -231,14 +259,24 @@ export function unbundle(result) {
return resource;
}

export async function findQuestionnaire(client, ref) {
export async function findQuestionnaireWithClient(client, ref) {
const query = ref.startsWith("http")
? `Questionnaire?url=${ref.replace(/\|.*$/, "")}`
: `Questionnaire/${ref.replace(/^Questionnaire\//, "")}`;

return Promise.any([
publicBuilderClient.request(query).then(unbundle),
client.request(query).then(unbundle),
publicBuilderClient.request(ref).then(unbundle),
publicBuilderClient
.request(query)
.then((result) => [publicBuilderClient, unbundle(result)]),
client.request(query).then((result) => [client, unbundle(result)]),
publicBuilderClient
.request(ref)
.then((result) => [publicBuilderClient, unbundle(result)]),
]);
}

export async function findQuestionnaire(client, ref) {
return findQuestionnaireWithClient(client, ref).then(
([, questionnaire]) => questionnaire,
);
}
5 changes: 1 addition & 4 deletions aidbox-forms-smart-launch/src/pages/error.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { useNavigate, useRouteError } from "react-router-dom";
import { useEffect, useLayoutEffect } from "react";
import { ChevronRight, Stethoscope, User } from "lucide-react";
import { Button } from "@/ui/button.jsx";
import { useRouteError } from "react-router-dom";
import { LaunchInstructions } from "@/components/launch-instructions.jsx";

export function Error() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { QuestionnaireBuilder } from "@/components/questionnaire-builder.jsx";
import { useParams } from "react-router-dom";
import { Suspense } from "react";

export const QuestionnaireEditor = () => {
const { id } = useParams();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useParams } from "react-router-dom";
import { publicBuilderClient, useClient } from "@/hooks/use-client.jsx";
import { useClient } from "@/hooks/use-client.jsx";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useAwaiter } from "@/hooks/use-awaiter.jsx";
import { useEffect, useRef } from "react";
Expand Down
34 changes: 5 additions & 29 deletions aidbox-forms-smart-launch/src/pages/questionnaire-responses.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMutation, useQueries, useQuery } from "@tanstack/react-query";
import { publicBuilderClient, useClient } from "@/hooks/use-client.jsx";
import { useQueries, useQuery } from "@tanstack/react-query";
import { useClient } from "@/hooks/use-client.jsx";
import { DataTable } from "@/components/data-table.jsx";
import { Button } from "@/ui/button";
import {
Expand All @@ -10,36 +10,12 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/ui/dropdown-menu";
import {
Copy,
Edit,
Eye,
Loader2,
MoreHorizontal,
Plus,
Trash,
Trash2,
} from "lucide-react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/ui/dialog.jsx";
import { Copy, Edit, MoreHorizontal } from "lucide-react";
import * as React from "react";
import { Suspense, useState } from "react";
import { Link, useNavigate, useSearchParams } from "react-router-dom";
import { QuestionnairePreview } from "@/components/questionnaire-preview.jsx";
import { Loading } from "@/components/loading.jsx";
import { useNavigate, useSearchParams } from "react-router-dom";
import { Pagination } from "@/components/pagination.jsx";
import { useLaunchContext } from "@/hooks/use-launch-context.jsx";
import {
constructName,
createQuestionnaireResponse,
findQuestionnaire,
} from "@/lib/utils.js";
import { useToast } from "@/hooks/use-toast.js";
import { Spinner } from "@/components/spinner.jsx";
import { constructName, findQuestionnaire } from "@/lib/utils.js";

export const QuestionnaireResponses = () => {
const [searchParams] = useSearchParams();
Expand Down
Loading

0 comments on commit 727178d

Please sign in to comment.