diff --git a/src/backend/routers/case_manager.ts b/src/backend/routers/case_manager.ts index 84c38f33..30f7f883 100644 --- a/src/backend/routers/case_manager.ts +++ b/src/backend/routers/case_manager.ts @@ -77,6 +77,48 @@ export const case_manager = router({ .executeTakeFirstOrThrow(); }), + /** + * Edits the given student in the CM's roster. Throws an error if the student was not found in the db. + */ + editStudent: authenticatedProcedure + .input( + z.object({ + student_id: z.string(), + first_name: z.string(), + last_name: z.string(), + email: z.string().email(), + grade: z.number(), + }) + ) + .mutation(async (req) => { + const { student_id, first_name, last_name, email, grade } = req.input; + const { userId } = req.ctx.auth; // case manager id + + // Check if the student exists and if the case manager is assigned to the student + const existingStudent = req.ctx.db + .selectFrom("student") + .selectAll() + .where("student_id", "=", student_id) + .where("assigned_case_manager_id", "=", userId); + + if (!existingStudent) { + throw new Error("Student not found"); + } + + // Update the student's information + return await req.ctx.db + .updateTable("student") + .set({ + first_name, + last_name, + email: email.toLowerCase(), + grade, + }) + .where("student_id", "=", student_id) + .returningAll() + .executeTakeFirstOrThrow(); + }), + /** * Removes the case manager associated with this student. */ diff --git a/src/backend/routers/para.ts b/src/backend/routers/para.ts index 309696ba..c5e735af 100644 --- a/src/backend/routers/para.ts +++ b/src/backend/routers/para.ts @@ -48,6 +48,8 @@ export const para = router({ .selectAll() .executeTakeFirst(); + const caseManagerName = req.ctx.auth.session.user?.name ?? ""; + if (!paraData) { paraData = await req.ctx.db .insertInto("user") @@ -66,10 +68,10 @@ export const para = router({ to: email, subject: "Para-professional email confirmation", text: "Email confirmation", - html: "

Email confirmation

Please confirm your email by going to the following link: no link yet

", + html: `

Dear ${first_name},

Welcome to the data collection team for SFUSD.EDU!

I am writing to invite you to join our data collection efforts for our students. We are using an online platform called Project Compass to track and monitor student progress, and your participation is crucial to the success of this initiative.

To access Project Compass and begin collecting data, please follow these steps:

By clicking on the data collection button, you will be directed to the instructions outlining the necessary steps for data collection. Simply follow the provided instructions and enter the required data points accurately.

If you encounter any difficulties or have any questions, please feel free to reach out to me. I am here to assist you throughout the process and ensure a smooth data collection experience. Your dedication and contribution will make a meaningful impact on our students' educational journeys.

Thank you,

${caseManagerName}
Case Manager

`, }); - // TODO: when site is deployed, add url to html above - // to do elsewhere: add "email_verified_at" timestamp when para first signs in with their email address (entered into db by cm) + // TODO: when site is deployed, add new url to html above + // TODO elsewhere: add "email_verified_at" timestamp when para first signs in with their email address (entered into db by cm) } return paraData; diff --git a/src/backend/trpc.ts b/src/backend/trpc.ts index ba33f371..d4184cc9 100644 --- a/src/backend/trpc.ts +++ b/src/backend/trpc.ts @@ -2,6 +2,7 @@ import { TRPCError, initTRPC } from "@trpc/server"; import { createContext } from "./context"; import superjson from "superjson"; +// initialize tRPC exactly once per application: export const t = initTRPC.context().create({ // SuperJSON allows us to transparently use, e.g., standard Date/Map/Sets // over the wire between the server and client. @@ -34,6 +35,7 @@ const isAdmin = t.middleware(({ next, ctx }) => { }); }); +// Define and export the tRPC router export const router = t.router; export const authenticatedProcedure = t.procedure.use(isAuthenticated); export const adminProcedure = t.procedure.use(isAuthenticated).use(isAdmin); diff --git a/src/components/iep/Iep.tsx b/src/components/iep/Iep.tsx index 4245bc9b..447040e3 100644 --- a/src/components/iep/Iep.tsx +++ b/src/components/iep/Iep.tsx @@ -5,10 +5,11 @@ import $input from "@/styles/Input.module.css"; import Box from "@mui/material/Box"; import Container from "@mui/material/Container"; import List from "@mui/material/List"; +import Stack from "@mui/material/Stack"; import Image from "next/image"; import noGoals from "../../public/img/no-goals-icon.png"; -import $Iep from "./Iep.module.css"; import $Image from "../../styles/Image.module.css"; +import $Iep from "./Iep.module.css"; interface IepProps { iep_id: string; @@ -42,7 +43,7 @@ const Iep = ({ iep_id }: IepProps) => { } return ( - <> +

Goals ({goals?.length ?? 0})

@@ -104,7 +105,7 @@ const Iep = ({ iep_id }: IepProps) => {
)} - +
); }; export default Iep; diff --git a/src/pages/students/[student_id].tsx b/src/pages/students/[student_id].tsx index 71f5d761..c9a9f20b 100644 --- a/src/pages/students/[student_id].tsx +++ b/src/pages/students/[student_id].tsx @@ -6,39 +6,78 @@ import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import Container from "@mui/material/Container"; import Modal from "@mui/material/Modal"; +import Stack from "@mui/material/Stack"; +import { addYears, format, parseISO, subDays } from "date-fns"; import Image from "next/image"; -import Link from "next/link"; import { useRouter } from "next/router"; import { useState } from "react"; import Iep from "../../components/iep/Iep"; import noGoals from "../../public/img/no-goals-icon.png"; import $Form from "../../styles/Form.module.css"; +import $Image from "../../styles/Image.module.css"; import $Modal from "../../styles/Modal.module.css"; import $StudentPage from "../../styles/StudentPage.module.css"; -import $Image from "../../styles/Image.module.css"; -import { parseISO, addYears, subDays, format } from "date-fns"; const ViewStudentPage = () => { const [createIepModal, setCreateIepModal] = useState(false); const [archivePrompt, setArchivePrompt] = useState(false); const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); + const [viewState, setViewState] = useState(0); const utils = trpc.useContext(); const router = useRouter(); const { student_id } = router.query; + const VIEW_STATES = { MAIN: 0, EDIT: 1 }; + + const handleEditState = () => { + setViewState(VIEW_STATES.EDIT); + }; + + const handleMainState = () => { + setViewState(VIEW_STATES.MAIN); + }; + const { data: student, isLoading } = trpc.student.getStudentById.useQuery( { student_id: student_id as string }, { enabled: Boolean(student_id) } ); + const buttonSX = { + "&:hover": { + background: "#3023B8", + }, + }; + const { data: activeIep } = trpc.student.getActiveStudentIep.useQuery( { student_id: student_id as string }, { enabled: Boolean(student_id), retry: false } ); + const editMutation = trpc.case_manager.editStudent.useMutation({ + onSuccess: () => utils.student.getStudentById.invalidate(), + }); + + const handleEditStudent = (e: React.ChangeEvent) => { + e.preventDefault(); + const data = new FormData(e.currentTarget); + + if (!student) { + return; // TODO: improve error handling + } + editMutation.mutate({ + student_id: student.student_id, + first_name: data.get("firstName") as string, + last_name: data.get("lastName") as string, + email: data.get("email") as string, + grade: Number(data.get("grade")) || 0, + }); + handleMainState(); + }; + const archiveMutation = trpc.case_manager.removeStudent.useMutation(); + const handleArchiveStudent = async () => { if (!student) { return; @@ -82,51 +121,202 @@ const ViewStudentPage = () => { } return ( -
- + +

{student?.first_name} {student?.last_name}

- {/* //Todo: Modify Edit Button */} - -
- - - -
-

Grade:

-

{student?.grade}

-
-
-

IEP End Date:

-

- {activeIep?.end_date.toLocaleDateString() ?? "None"} -

-
-
- {/* // TODO: Extract 'Archive Student' to 'Edit' and 'Return to Student List' somewhere */} - - - - Return to Student List - - + Edit + + )} + + {/* Save and Cancel buttons only to be shown when view state is set to EDIT */} + {viewState === VIEW_STATES.EDIT && ( + + + + + )}
+ + {/* if view state is "EDIT" then show the edit version of the student page */} + {viewState === VIEW_STATES.EDIT &&

Edit Profile

} + + {viewState === VIEW_STATES.MAIN && ( + + +
+

Grade

+

{student?.grade}

+
+
+

Next IEP

+

+ {activeIep?.end_date.toLocaleDateString() ?? "None"} +

+
+
+
+ )}
- {/* If no IEP, prompt CM to create one */} - {!activeIep ? ( + {viewState === VIEW_STATES.EDIT ? ( + +
+ + + +

:

+ +
+ + +

:

+ +
+ + +

:

+ +
+ + +

:

+ +
+
+
+ + + + + + +
+ ) : !activeIep ? ( { )} - {/* Archiving Student Modal*/} + {/* Archiving Student Modal appears when "Archive" button is pressed*/} setArchivePrompt(false)} @@ -241,7 +431,7 @@ const ViewStudentPage = () => { -
+ ); }; diff --git a/src/pages/students/index.tsx b/src/pages/students/index.tsx index 4e9906ff..ea698917 100644 --- a/src/pages/students/index.tsx +++ b/src/pages/students/index.tsx @@ -19,6 +19,10 @@ const Students = () => { ), }); + // create editStudent + + // make a separate handleEdit?? + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); const data = new FormData(event.currentTarget); diff --git a/src/styles/StudentPage.module.css b/src/styles/StudentPage.module.css index cba8c9aa..350d3e0d 100644 --- a/src/styles/StudentPage.module.css +++ b/src/styles/StudentPage.module.css @@ -1,6 +1,13 @@ .studentInfoContainer { background-color: #ffffff; border-radius: 10px; + padding: 0.5rem; +} + +.studentEditContainer { + background-color: #ffffff; + border-radius: 10px; + padding: 20px 50px; } .displayBox { @@ -9,6 +16,13 @@ padding: 10px 0; } +.displayBoxGap { + display: flex; + justify-content: space-between; + padding: 10px 0; + gap: 1rem; +} + .studentName { font-size: xx-large; } @@ -17,6 +31,10 @@ display: flex; } +.editForm { + width: 100%; +} + .singleInfoArea { padding: 10px 0; text-align: center; diff --git a/src/styles/globals.css b/src/styles/globals.css index f605af13..25447fe3 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -115,6 +115,13 @@ textarea { resize: vertical; } +input[type="text"], +input[type="password"], +textarea { + border: none; + outline: none; +} + /* Breakpoints recommended by Bootstrap */ /* Custom, iPhone Retina */ @media only screen and (min-width: 320px) {