Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ttp 40 questionnaires form #4

Merged
merged 4 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"use client";

import type { UseFormReturn } from "react-hook-form";

import { Checkbox } from "@acme/ui/checkbox";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@acme/ui/form";

import type { Question } from "./input-question";

interface CheckboxQuestionProps {
form: UseFormReturn<any, any, undefined>;
question: Question;
}

export const CheckboxQuestion = (props: CheckboxQuestionProps) => {
const { form, question } = props;

return (
<FormField
control={form.control}
name={question.linkId!}
render={() => (
<FormItem>
<div className="mb-4">
<FormLabel className="text-base">{question.text}</FormLabel>
</div>
{question.answerOption?.map((option) => (
<FormField
key={option.valueCoding?.code}
control={form.control}
name={question.linkId!}
render={({ field }) => {
const isChecked = field.value?.some(
(selectedOption) =>
selectedOption.code === option.valueCoding?.code,
);
return (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={isChecked}
onCheckedChange={(checked) => {
const existingValues = field.value || [];
if (checked) {
field.onChange([
...existingValues,
option.valueCoding,
]);
} else {
field.onChange(
existingValues.filter(
(selectedOption) =>
selectedOption.code !==
option.valueCoding?.code,
),
);
}
}}
/>
</FormControl>
<FormLabel className="font-normal">
{option.valueCoding?.display}
</FormLabel>
</FormItem>
);
}}
/>
))}
<FormMessage />
</FormItem>
)}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import type { UseFormReturn } from "react-hook-form";
import type { z } from "zod";

import type { get_ReadQuestionnaire } from "@acme/api/src/canvas/canvas-client";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@acme/ui/form";
import { Input } from "@acme/ui/input";

export type Question = z.infer<
typeof get_ReadQuestionnaire.response.shape.item._def.innerType.element
>;

interface InputQuestionProps {
form: UseFormReturn<any, any, undefined>;
question: Question;
}

export const InputQuestion = (props: InputQuestionProps) => {
const { form, question } = props;

return (
<FormField
control={form.control}
name="answerOption"
render={({ field }) => (
<>
{question.answerOption?.map((option) => (
<FormItem key={option.valueCoding?.code}>
<FormLabel>{option.valueCoding?.display}</FormLabel>
<FormControl>
<Input placeholder="" {...field} />
</FormControl>
<FormMessage />
</FormItem>
))}
</>
)}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"use client";

import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { z } from "zod";
import type { ZodSchema } from "zod";

import { generateQuestionnaireSchema } from "@acme/api/src/validators";
import { Button } from "@acme/ui/button";
import { Form } from "@acme/ui/form";
import { useToast } from "@acme/ui/use-toast";

import { useZodForm } from "~/lib/zod-form";
import { api } from "~/trpc/react";
import { CheckboxQuestion } from "./checkbox-question";
import { InputQuestion } from "./input-question";
import { RadioQuestion } from "./radio-question";

interface QuestionnaireProps {
questionnaireId: string;
onSuccess: () => void;
}

export function QuestionnaireForm(props: QuestionnaireProps) {
const { questionnaireId, onSuccess } = props;

const { isLoading, isError, data, error } =
api.canvas.getQuestionnaire.useQuery({
id: questionnaireId,
});

const mutation = api.canvas.submitQuestionnaireResponse.useMutation();

const router = useRouter();
const toaster = useToast();

const [dynamicSchema, setDynamicSchema] = useState<ZodSchema | null>(null);

useEffect(() => {
if (data) {
const questionnaireSchema = generateQuestionnaireSchema(data);
setDynamicSchema(questionnaireSchema);
}
}, [data]);

const form = useZodForm({
schema: dynamicSchema ?? z.any(),
defaultValues: {},
});

const items = data?.item;

function onSubmit(formData: unknown) {
const transformedItems = items?.map((question) => {
let answers;

if (question.type === "choice") {
if (question.repeats) {
// For checkbox questions, formData contains an array of valueCoding objects
answers = formData[question.linkId].map((valueCoding) => ({
valueCoding,
}));
} else {
// For radio questions, formData contains a single valueCoding object
answers = [{ valueCoding: formData[question.linkId] }];
}
} else if (question.type === "text") {
// For text questions, formData contains a string
answers = [{ valueString: formData[question.linkId] }];
}

return {
linkId: question.linkId,
text: question.text,
answer: answers,
};
});

const requestBody = {
questionnaire: `Questionnaire/${questionnaireId}`,
status: "completed",
subject: {
reference: `Patient/b685d0d97f604e1fb60f9ed089abc410`, // TODO
type: "Patient",
},
item: transformedItems,
};

try {
console.log(data, "data");
console.log(formData, "formData");
console.log(transformedItems, "transformedItems");
mutation.mutate({
body: requestBody,
});
if (mutation.isSuccess) {
toaster.toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">
{JSON.stringify(data, null, 2)}
</code>
</pre>
),
});
onSuccess();
} else {
// router.push(`/onboarding`);
}
} catch (error) {
toaster.toast({
title: "Error submitting answer",
variant: "destructive",
description:
"An issue occurred while submitting answer. Please try again.",
});
}
}

if (isLoading) {
return <span>Loading...</span>;
}

if (isError) {
return <span>Error: {error.message}</span>;
}

return (
<>
{dynamicSchema && (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="w-2/3 space-y-6"
>
{items?.map((question, index) => {
switch (question.type) {
case "choice":
return question.repeats ? (
<CheckboxQuestion
key={index}
form={form}
question={question}
/>
) : (
<RadioQuestion
key={index}
form={form}
question={question}
/>
);
case "text":
return (
<InputQuestion
key={index}
form={form}
question={question}
/>
);
default:
console.warn("Unsupported question type:", question.type);
return null;
}
})}
<Button type="submit" variant="outline">
Submit
</Button>
</form>
</Form>
)}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client";

import { useRouter } from "next/navigation";
import { motion } from "framer-motion";
import { Balancer } from "react-wrap-balancer";

import { QuestionnaireForm } from "./questionnaire-form";

export default function Questionnaire() {
const router = useRouter();

return (
<motion.div
className="my-auto flex h-full w-full flex-col items-center justify-center"
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.3, type: "spring" }}
>
<motion.div
variants={{
show: {
transition: {
staggerChildren: 0.2,
},
},
}}
initial="hidden"
animate="show"
className="flex flex-col rounded-xl bg-background/60 p-8"
>
<motion.h1
className="mb-4 font-cal text-2xl font-bold transition-colors sm:text-3xl"
variants={{
hidden: { opacity: 0, x: 250 },
show: {
opacity: 1,
x: 0,
transition: { duration: 0.4, type: "spring" },
},
}}
>
<Balancer>{`Questionnaire`}</Balancer>
</motion.h1>
<motion.div
variants={{
hidden: { opacity: 0, x: 100 },
show: {
opacity: 1,
x: 0,
transition: { duration: 0.4, type: "spring" },
},
}}
>
<QuestionnaireForm
questionnaireId="f62257a5-bf65-4678-b8d1-568bd298617d"
onSuccess={() => router.push("/onboarding?step=questionnaire")} // do ?step=review
/>
</motion.div>
</motion.div>
</motion.div>
);
}
Loading
Loading