diff --git a/src/backend/lib/db_helpers/case_manager.ts b/src/backend/lib/db_helpers/case_manager.ts new file mode 100644 index 00000000..6073ece3 --- /dev/null +++ b/src/backend/lib/db_helpers/case_manager.ts @@ -0,0 +1,94 @@ +import { Env } from "@/backend/lib/types"; +import { KyselyDatabaseInstance } from "@/backend/lib"; +import { getTransporter } from "@/backend/lib/nodemailer"; +import { user } from "zapatos/schema"; + +interface paraInputProps { + first_name: string; + last_name: string; + email: string; +} + +/** + * Checks for the existence of a user with the given email, if + * they do not exist, create the user with the role of "staff", + * initiate email sending without awaiting result + */ +export async function createPara( + para: paraInputProps, + db: KyselyDatabaseInstance, + case_manager_id: string, + from_email: string, + to_email: string, + env: Env +): Promise { + const { first_name, last_name, email } = para; + + let paraData = await db + .selectFrom("user") + .where("email", "=", email.toLowerCase()) + .selectAll() + .executeTakeFirst(); + + if (!paraData) { + paraData = await db + .insertInto("user") + .values({ + first_name, + last_name, + email: email.toLowerCase(), + role: "staff", + }) + .returningAll() + .executeTakeFirst(); + + // promise, will not interfere with returning paraData + void sendInviteEmail( + from_email, + to_email, + first_name, + case_manager_id, + env + ); + } + + return paraData; +} + +/** + * Sends an invitation email to a para + */ +export async function sendInviteEmail( + fromEmail: string, + toEmail: string, + first_name: string, + caseManagerName: string, + env: Env +): Promise { + await getTransporter(env).sendMail({ + from: fromEmail, + to: toEmail, + subject: "Para-professional email confirmation", + text: "Email confirmation", + 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

`, + }); + return; +} + +/** + * Takes a given user (para), another user (cm), and a Kysely database, + * creates a link between the users in the database's + * "paras_assigned_to_case_manager" table + */ +export async function assignParaToCaseManager( + para_id: string, + case_manager_id: string, + db: KyselyDatabaseInstance +): Promise { + await db + .insertInto("paras_assigned_to_case_manager") + .values({ case_manager_id, para_id }) + .execute(); + + return; +} diff --git a/src/backend/routers/case_manager.test.ts b/src/backend/routers/case_manager.test.ts index 96fb9373..870ebf5d 100644 --- a/src/backend/routers/case_manager.test.ts +++ b/src/backend/routers/case_manager.test.ts @@ -200,6 +200,31 @@ test("getMyParas", async (t) => { t.is(myParas.length, 1); }); +test("addStaff", async (t) => { + const { trpc } = await getTestServer(t, { + authenticateAs: "case_manager", + }); + + const parasBeforeAdd = await trpc.case_manager.getMyParas.query(); + t.is(parasBeforeAdd.length, 0); + + const newParaData = { + first_name: "Staffy", + last_name: "Para", + email: "sp@gmail.com", + }; + + await trpc.case_manager.addStaff.mutate(newParaData); + + const parasAfterAdd = await trpc.case_manager.getMyParas.query(); + t.is(parasAfterAdd.length, 1); + + const createdPara = parasAfterAdd[0]; + t.is(createdPara.first_name, newParaData.first_name); + t.is(createdPara.last_name, newParaData.last_name); + t.is(createdPara.email, newParaData.email); +}); + test("addPara", async (t) => { const { trpc, seed } = await getTestServer(t, { authenticateAs: "case_manager", diff --git a/src/backend/routers/case_manager.ts b/src/backend/routers/case_manager.ts index bd045b95..175b74f4 100644 --- a/src/backend/routers/case_manager.ts +++ b/src/backend/routers/case_manager.ts @@ -1,5 +1,9 @@ import { z } from "zod"; import { authenticatedProcedure, router } from "../trpc"; +import { + createPara, + assignParaToCaseManager, +} from "../lib/db_helpers/case_manager"; export const case_manager = router({ /** @@ -156,6 +160,38 @@ export const case_manager = router({ return result; }), + /** + * Handles creation of para and assignment to user, attempts to send + * email but does not await email success + */ + addStaff: authenticatedProcedure + .input( + z.object({ + first_name: z.string(), + last_name: z.string(), + email: z.string().email(), + }) + ) + .mutation(async (req) => { + const para = await createPara( + req.input, + req.ctx.db, + req.ctx.auth.userId, + req.ctx.env.EMAIL, + req.input.email, + req.ctx.env + ); + + return await assignParaToCaseManager( + para?.user_id || "", + req.ctx.auth.userId, + req.ctx.db + ); + }), + + /** + * Deprecated: use addStaff instead + */ addPara: authenticatedProcedure .input( z.object({ @@ -163,13 +199,12 @@ export const case_manager = router({ }) ) .mutation(async (req) => { - const { para_id } = req.input; - const { userId } = req.ctx.auth; - - await req.ctx.db - .insertInto("paras_assigned_to_case_manager") - .values({ case_manager_id: userId, para_id }) - .execute(); + await assignParaToCaseManager( + req.input.para_id, + req.ctx.auth.userId, + req.ctx.db + ); + return; }), editPara: authenticatedProcedure diff --git a/src/backend/routers/para.ts b/src/backend/routers/para.ts index c5e735af..be06935c 100644 --- a/src/backend/routers/para.ts +++ b/src/backend/routers/para.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { getTransporter } from "../lib/nodemailer"; import { authenticatedProcedure, router } from "../trpc"; +import { createPara } from "../lib/db_helpers/case_manager"; export const para = router({ getParaById: authenticatedProcedure @@ -31,6 +32,9 @@ export const para = router({ return result; }), + /** + * Deprecated: use case_manager.addStaff instead + */ createPara: authenticatedProcedure .input( z.object({ @@ -42,39 +46,20 @@ export const para = router({ .mutation(async (req) => { const { first_name, last_name, email } = req.input; - let paraData = await req.ctx.db - .selectFrom("user") - .where("email", "=", email.toLowerCase()) - .selectAll() - .executeTakeFirst(); - - const caseManagerName = req.ctx.auth.session.user?.name ?? ""; - - if (!paraData) { - paraData = await req.ctx.db - .insertInto("user") - .values({ - first_name, - last_name, - email: email.toLowerCase(), - role: "staff", - }) - .returningAll() - .executeTakeFirst(); + const para = await createPara( + req.input, + req.ctx.db, + req.ctx.auth.session.user?.name ?? "", + req.ctx.env.EMAIL, + email, + req.ctx.env + ); - // TODO: Logic for sending email to staff. Should email be sent everytime or only first time? Should staff be notified that they are added to a certain case manager's list? - await getTransporter(req.ctx.env).sendMail({ - from: req.ctx.env.EMAIL, - to: email, - subject: "Para-professional email confirmation", - text: "Email confirmation", - 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:

  • Go to the website: (https://staging.compassiep.com/)
  • Login using your provided username and password
  • Once logged in, navigate to the dashboard where you would see the student goals page

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 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 para; - return paraData; + // TODO: Logic for sending email to staff. Should email be sent everytime or only first time? Should staff be notified that they are added to a certain case manager's list? + // 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) }), getMyTasks: authenticatedProcedure.query(async (req) => { diff --git a/src/pages/staff/index.tsx b/src/pages/staff/index.tsx index 10c39769..c66e8868 100644 --- a/src/pages/staff/index.tsx +++ b/src/pages/staff/index.tsx @@ -7,15 +7,7 @@ const Staff = () => { const { data: paras, isLoading } = trpc.case_manager.getMyParas.useQuery(); const { data: me } = trpc.user.getMe.useQuery(); - const createPara = trpc.para.createPara.useMutation({ - onSuccess: async (data) => { - await assignParaToCaseManager.mutateAsync({ - para_id: data?.user_id as string, - }); - }, - }); - - const assignParaToCaseManager = trpc.case_manager.addPara.useMutation({ + const addStaff = trpc.case_manager.addStaff.useMutation({ onSuccess: () => utils.case_manager.getMyParas.invalidate(), }); @@ -24,7 +16,7 @@ const Staff = () => { const data = new FormData(event.currentTarget); try { - await createPara.mutateAsync({ + await addStaff.mutateAsync({ first_name: data.get("first_name") as string, last_name: data.get("last_name") as string, email: data.get("email") as string,