diff --git a/package.json b/package.json index adaac655..06d7d6cb 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "type-css:check": "npx tcm src --listDifferent", "test": "next build --no-lint && ava", "test-storybook": "test-storybook --maxWorkers=2", + "docker-reup": "docker compose -f supporting_services/docker-compose.yml up -d", "db:migrate": "npx tsx src/backend/scripts/migrate-database.ts", "db:reset": "npx tsx src/backend/scripts/reset-database.ts", "db:seed": "npx tsx src/backend/scripts/seed-database.ts", diff --git a/src/backend/routers/iep.test.ts b/src/backend/routers/iep.test.ts index f37b97b3..e61c2550 100644 --- a/src/backend/routers/iep.test.ts +++ b/src/backend/routers/iep.test.ts @@ -94,7 +94,7 @@ test("basic flow - add/get goals, benchmarks, tasks", async (t) => { const gotBenchmark = await trpc.iep.getBenchmark.query({ benchmark_id: benchmark2Id, }); - t.is(gotBenchmark[0].description, "benchmark 2"); + t.is(gotBenchmark.description, "benchmark 2"); // TODO: Don't query db directly and use an API method instead. Possibly create a getTasks method later t.truthy( diff --git a/src/backend/routers/iep.ts b/src/backend/routers/iep.ts index 5db3a2d3..29ab56ed 100644 --- a/src/backend/routers/iep.ts +++ b/src/backend/routers/iep.ts @@ -363,14 +363,87 @@ export const iep = router({ .query(async (req) => { const { benchmark_id } = req.input; + // NOTE: existing code const result = await req.ctx.db .selectFrom("benchmark") + .innerJoin("task", "benchmark.benchmark_id", "task.benchmark_id") + .innerJoin("user", "task.assignee_id", "user.user_id") .where("benchmark.benchmark_id", "=", benchmark_id) - .selectAll() - .execute(); + .select((eb) => [ + "benchmark.description", + "benchmark.instructions", + "benchmark.frequency", + "benchmark.number_of_trials", + "benchmark.benchmark_id", + jsonArrayFrom( + eb + .selectFrom("user") + .selectAll() + .whereRef("task.assignee_id", "=", "user.user_id") + .orderBy("user.first_name") + ).as("assignees"), + ]) + .executeTakeFirstOrThrow(); return result; }), + //.innerJoin("task", "benchmark.benchmark_id", "task.benchmark_id") + // .innerJoin("goal", "benchmark.goal_id", "goal.goal_id") + // .innerJoin("iep", "goal.iep_id", "iep.iep_id") + // .innerJoin("student", "iep.student_id", "student.student_id") + + // "task.task_id", + // "student.first_name", + // "student.last_name", + // "goal.category", + // "benchmark.description", + // "benchmark.instructions", + // "benchmark.frequency", + // "benchmark.number_of_trials", + // "benchmark.benchmark_id", + // "task.due_date", + // "task.seen", + // "task.trial_count", + + // .select([ + // "benchmark.description", + // "benchmark.instructions", + // "benchmark.frequency", + // "benchmark.number_of_trials", + // "benchmark.benchmark_id", + // ]) + + // { + // benchmark: { + // id, + // title, + // desc, + // assignees: [ + // { id:, name } + // ] + // } + // } + + // jsonArrayFrom( + // eb + // .selectFrom("trial_data") + // .select([ + // "trial_data.trial_data_id", + // "trial_data.success", + // "trial_data.unsuccess", + // "trial_data.submitted", + // "trial_data.notes", + // "trial_data.created_at", + // ]) + // .whereRef("trial_data.task_id", "=", "task.task_id") + // .whereRef( + // "trial_data.created_by_user_id", + // "=", + // "task.assignee_id" + // ) + // .orderBy("trial_data.created_at") + // ).as("assignees"), + getBenchmarkByAssignee: hasCaseManager .input( z.object({ diff --git a/src/client/lib/trpc.ts b/src/client/lib/trpc.ts index 10b65b63..32aea15e 100644 --- a/src/client/lib/trpc.ts +++ b/src/client/lib/trpc.ts @@ -1,4 +1,12 @@ -import { createTRPCReact } from "@trpc/react-query"; +import { + createTRPCReact, + type inferReactQueryProcedureOptions, +} from "@trpc/react-query"; +import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import { AppRouter } from "@/backend/routers/_app"; +export type ReactQueryOptions = inferReactQueryProcedureOptions; +export type RouterInputs = inferRouterInputs; +export type RouterOutputs = inferRouterOutputs; + export const trpc = createTRPCReact(); diff --git a/src/components/benchmarks/BenchmarkAssignment.tsx b/src/components/benchmarks/BenchmarkAssignment.tsx new file mode 100644 index 00000000..eb0853d3 --- /dev/null +++ b/src/components/benchmarks/BenchmarkAssignment.tsx @@ -0,0 +1,110 @@ +import { trpc } from "@/client/lib/trpc"; +import { useState, useRef } from "react"; + +import { AssignmentDuration } from "./Duration-Selection-Step"; +import { BenchmarkAssignmentModal } from "@/components/benchmarks/BenchmarkAssignmentModal"; + +interface BenchmarkAssignmentProps { + isOpen: boolean; + onClose: () => void; + benchmark_id: string; +} + +export const STEPS = ["PARA_SELECTION", "DURATION_SELECTION"]; +export type Step = (typeof STEPS)[number]; + +export const BenchmarkAssignment = (props: BenchmarkAssignmentProps) => { + const [selectedParaIds, setSelectedParaIds] = useState([]); + const nextButtonRef = useRef(null); + const [assignmentDuration, setAssignmentDuration] = + useState({ type: "forever" }); + const [currentModalSelection, setCurrentModalSelection] = + useState("PARA_SELECTION"); + const { data: myParas } = trpc.case_manager.getMyParas.useQuery(); + const { data: benchmark } = trpc.iep.getBenchmark.useQuery({ + benchmark_id: props.benchmark_id, + }); // maybe it should include assignments, or have a flag to include assignments + + const [errorMessage, setErrorMessage] = useState(""); + + const assignTaskToPara = trpc.iep.assignTaskToParas.useMutation(); + + const handleParaToggle = (paraId: string) => () => { + setErrorMessage(""); + setSelectedParaIds((prev) => { + if (prev.includes(paraId)) { + return prev.filter((id) => id !== paraId); + } else { + return [...prev, paraId]; + } + }); + }; + + const handleClose = () => { + props.onClose(); + setSelectedParaIds([]); + setErrorMessage(""); + setCurrentModalSelection("PARA_SELECTION"); + }; + + const handleBack = () => { + const currentStepIndex = STEPS.indexOf(currentModalSelection); + const previousStep = STEPS[currentStepIndex - 1]; + if (previousStep) { + setCurrentModalSelection(previousStep); + } + }; + + const handleNext = async () => { + if (nextButtonRef.current) { + nextButtonRef.current.blur(); + } + const currentStepIndex = STEPS.indexOf(currentModalSelection); + const nextStep = STEPS[currentStepIndex + 1]; + if (nextStep) { + setCurrentModalSelection(nextStep); + } else { + // Reached end, save + try { + await assignTaskToPara.mutateAsync({ + benchmark_id: props.benchmark_id, + para_ids: selectedParaIds, + due_date: + assignmentDuration.type === "until_date" + ? assignmentDuration.date + : undefined, + trial_count: + assignmentDuration.type === "minimum_number_of_collections" + ? assignmentDuration.minimumNumberOfCollections + : undefined, + }); + handleClose(); + } catch (err) { + // TODO: issue #450 + console.log(err); + if (err instanceof Error) { + setErrorMessage(err.message); + } + } + } + }; + + return ( + + ); +}; diff --git a/src/components/benchmarks/BenchmarkAssignmentModal.tsx b/src/components/benchmarks/BenchmarkAssignmentModal.tsx index 2d52868d..15383223 100644 --- a/src/components/benchmarks/BenchmarkAssignmentModal.tsx +++ b/src/components/benchmarks/BenchmarkAssignmentModal.tsx @@ -1,126 +1,62 @@ -import { trpc } from "@/client/lib/trpc"; import { Box, - Dialog, - DialogTitle, Button, - List, - ListItem, - DialogContent, + Dialog, DialogActions, + DialogContent, + DialogTitle, } from "@mui/material"; -import { useState, useRef } from "react"; -import $benchmark from "./BenchmarkAssignmentModal.module.css"; -import $button from "@/components/design_system/button/Button.module.css"; - import { AssignmentDuration, DurationSelectionStep, -} from "./Duration-Selection-Step"; -import DS_Checkbox from "../design_system/checkbox/Checkbox"; +} from "@/components/benchmarks/Duration-Selection-Step"; +import { ParaSelectionStep } from "@/components/benchmarks/ParaSelectionStep"; + +import $benchmark from "./BenchmarkAssignmentModal.module.css"; +import $button from "../design_system/button/Button.module.css"; +import { RouterOutputs } from "@/client/lib/trpc"; +import { Step, STEPS } from "@/components/benchmarks/BenchmarkAssignment"; +import { RefObject } from "react"; interface BenchmarkAssignmentModalProps { isOpen: boolean; - onClose: () => void; - benchmark_id: string; + handleClose: () => void; + benchmark: RouterOutputs["iep"]["getBenchmark"] | undefined; + myParas: RouterOutputs["case_manager"]["getMyParas"] | undefined; + currentModalSelection: Step; + errorMessage: string; + selectedParaIds: string[]; + handleParaToggle: (paraId: string) => () => void; + assignmentDuration: AssignmentDuration; + setAssignmentDuration: (assignmentDuration: AssignmentDuration) => void; + isAssignTaskToParaLoading: boolean; + handleBack: () => void; + handleNext: () => Promise; + nextButtonRef: RefObject; } -interface ParaProps { - role: string; - first_name: string; - last_name: string; - email: string; - para_id: string; - case_manager_id: string; - user_id: string; - email_verified_at: Date | null; - image_url: string | null; -} - -const STEPS = ["PARA_SELECTION", "DURATION_SELECTION"]; -type Step = (typeof STEPS)[number]; - -export const BenchmarkAssignmentModal = ( - props: BenchmarkAssignmentModalProps -) => { - const [selectedParaIds, setSelectedParaIds] = useState([]); - const nextButtonRef = useRef(null); - const [assignmentDuration, setAssignmentDuration] = - useState({ type: "forever" }); - const [currentModalSelection, setCurrentModalSelection] = - useState("PARA_SELECTION"); - const { data: myParas } = trpc.case_manager.getMyParas.useQuery(); - const { data: benchmark } = trpc.iep.getBenchmark.useQuery({ - benchmark_id: props.benchmark_id, - }); - - const [errorMessage, setErrorMessage] = useState(""); - - const assignTaskToPara = trpc.iep.assignTaskToParas.useMutation(); - - const handleParaToggle = (paraId: string) => () => { - setErrorMessage(""); - setSelectedParaIds((prev) => { - if (prev.includes(paraId)) { - return prev.filter((id) => id !== paraId); - } else { - return [...prev, paraId]; - } - }); - }; - - const handleClose = () => { - props.onClose(); - setSelectedParaIds([]); - setErrorMessage(""); - setCurrentModalSelection("PARA_SELECTION"); - }; - - const handleBack = () => { - const currentStepIndex = STEPS.indexOf(currentModalSelection); - const previousStep = STEPS[currentStepIndex - 1]; - if (previousStep) { - setCurrentModalSelection(previousStep); - } - }; - - const handleNext = async () => { - if (nextButtonRef.current) { - nextButtonRef.current.blur(); - } - const currentStepIndex = STEPS.indexOf(currentModalSelection); - const nextStep = STEPS[currentStepIndex + 1]; - if (nextStep) { - setCurrentModalSelection(nextStep); - } else { - // Reached end, save - try { - await assignTaskToPara.mutateAsync({ - benchmark_id: props.benchmark_id, - para_ids: selectedParaIds, - due_date: - assignmentDuration.type === "until_date" - ? assignmentDuration.date - : undefined, - trial_count: - assignmentDuration.type === "minimum_number_of_collections" - ? assignmentDuration.minimumNumberOfCollections - : undefined, - }); - handleClose(); - } catch (err) { - // TODO: issue #450 - console.log(err); - if (err instanceof Error) { - setErrorMessage(err.message); - } - } - } - }; - +export const BenchmarkAssignmentModal = ({ + isOpen, + handleClose, + benchmark, + myParas, + currentModalSelection, + errorMessage, + selectedParaIds, + handleParaToggle, + assignmentDuration, + setAssignmentDuration, + isAssignTaskToParaLoading, + handleBack, + handleNext, + nextButtonRef, +}: BenchmarkAssignmentModalProps) => { + if (!benchmark) { + return
Loading!
; + } return (

Benchmark

- {benchmark?.map((thisBenchmark) => ( -

- {thisBenchmark.description} -

- ))} +

+ {benchmark.description} +

{currentModalSelection === "PARA_SELECTION" && ( - -

Select one or more paras:

- - - {myParas - ?.filter((para): para is ParaProps => para !== undefined) - .map((para) => ( - - - - ))} - - -
+ )} {currentModalSelection === "DURATION_SELECTION" && ( )} @@ -196,7 +103,7 @@ export const BenchmarkAssignmentModal = ( @@ -206,9 +113,7 @@ export const BenchmarkAssignmentModal = ( className={$button.default} onClick={handleNext} ref={nextButtonRef} - disabled={ - assignTaskToPara.isLoading || selectedParaIds.length === 0 - } + disabled={isAssignTaskToParaLoading || selectedParaIds.length === 0} > {currentModalSelection === STEPS[STEPS.length - 1] ? "Save" diff --git a/src/components/benchmarks/BenchmarkListElement.tsx b/src/components/benchmarks/BenchmarkListElement.tsx index 24d22297..649e2f59 100644 --- a/src/components/benchmarks/BenchmarkListElement.tsx +++ b/src/components/benchmarks/BenchmarkListElement.tsx @@ -4,7 +4,7 @@ import Button from "@mui/material/Button"; import Divider from "@mui/material/Divider"; import ContentPasteIcon from "@mui/icons-material/ContentPaste"; import { useState, type ReactNode } from "react"; -import { BenchmarkAssignmentModal } from "./BenchmarkAssignmentModal"; +import { BenchmarkAssignment } from "./BenchmarkAssignment"; import $button from "@/components/design_system/button/Button.module.css"; import { format } from "date-fns"; import Typography from "@mui/material/Typography"; @@ -173,7 +173,7 @@ const BenchmarkListElement = ({ benchmark, index }: BenchmarkProps) => { - setIsAssignmentModalOpen(false)} benchmark_id={benchmark.benchmark_id} diff --git a/src/components/benchmarks/ParaSelectionStep.tsx b/src/components/benchmarks/ParaSelectionStep.tsx new file mode 100644 index 00000000..39f7a9a5 --- /dev/null +++ b/src/components/benchmarks/ParaSelectionStep.tsx @@ -0,0 +1,51 @@ +import { Box, List, ListItem } from "@mui/material"; +import $benchmark from "./BenchmarkAssignmentModal.module.css"; +import { RouterOutputs } from "@/client/lib/trpc"; +import DS_Checkbox from "@/components/design_system/checkbox/Checkbox"; + +interface ParaSelectionStepProps { + myParas: RouterOutputs["case_manager"]["getMyParas"] | undefined; + selectedParaIds: string[]; + handleParaToggle: (paraId: string) => () => void; +} + +export const ParaSelectionStep = ({ + myParas, + selectedParaIds, + handleParaToggle, +}: ParaSelectionStepProps) => { + return ( + +

Select one or more paras:

+ + + {myParas + ?.filter((para) => para !== undefined) + .map((para) => ( + + + + ))} + + +
+ ); +};