From 3a55d0a32985e81c4d790773f1c6402f69a43f45 Mon Sep 17 00:00:00 2001 From: Connor Wales Date: Tue, 2 Apr 2024 22:42:08 -0700 Subject: [PATCH 1/6] break out logic from routers/para.createPara to backend/lib.db_helpers/para.ts, appears to be working from frontend --- src/backend/lib/db_helpers/para.ts | 65 ++++++++++++++++++++++++++++++ src/backend/routers/para.ts | 12 ++++++ src/pages/staff/index.tsx | 3 ++ 3 files changed, 80 insertions(+) create mode 100644 src/backend/lib/db_helpers/para.ts diff --git a/src/backend/lib/db_helpers/para.ts b/src/backend/lib/db_helpers/para.ts new file mode 100644 index 00000000..463dec08 --- /dev/null +++ b/src/backend/lib/db_helpers/para.ts @@ -0,0 +1,65 @@ +import { Env } from "@/backend/lib/types"; +import { KyselyDatabaseInstance } from "@/backend/lib"; +import { getTransporter } from "@/backend/lib/nodemailer"; + +interface paraInputProps { + first_name: string; + last_name: string; + email: string; +} + +export async function createPara( + para: paraInputProps, + db: KyselyDatabaseInstance, + caseManagerName: string, + fromEmail: string, + toEmail: string, + env: Env +) { + 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(fromEmail, toEmail, first_name, caseManagerName, env); + } + + return paraData; +} + +export async function sendInviteEmail( + fromEmail: string, + toEmail: string, + first_name: string, + caseManagerName: string, + env: Env +) { + 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

`, + }); +} + +// todo +// export async function assignParaToCaseManager() { +// +// } diff --git a/src/backend/routers/para.ts b/src/backend/routers/para.ts index c5e735af..e717460a 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/para"; export const para = router({ getParaById: authenticatedProcedure @@ -42,6 +43,17 @@ export const para = router({ .mutation(async (req) => { const { first_name, last_name, email } = req.input; + const para = await createPara( + req.input, + req.ctx.db, + req.ctx.auth.session.user?.name ?? "", + req.ctx.env.EMAIL, + email, + req.ctx.env + ); + + return para; + let paraData = await req.ctx.db .selectFrom("user") .where("email", "=", email.toLowerCase()) diff --git a/src/pages/staff/index.tsx b/src/pages/staff/index.tsx index 10c39769..ec04193f 100644 --- a/src/pages/staff/index.tsx +++ b/src/pages/staff/index.tsx @@ -7,8 +7,11 @@ const Staff = () => { const { data: paras, isLoading } = trpc.case_manager.getMyParas.useQuery(); const { data: me } = trpc.user.getMe.useQuery(); + // const addPara = trpc.para.addPara.useMutation({}) + const createPara = trpc.para.createPara.useMutation({ onSuccess: async (data) => { + console.log(data); await assignParaToCaseManager.mutateAsync({ para_id: data?.user_id as string, }); From 0f49c9c5f0e29e77964f5c2604a33c03ebf4ad78 Mon Sep 17 00:00:00 2001 From: Connor Wales Date: Wed, 3 Apr 2024 21:23:31 -0700 Subject: [PATCH 2/6] change db_helpers/para to db_helpers/case_manager, create addStaff procedure in routers/case_manager to handle para creation, email, and assignment, change staff/index.tsx handleSubmit to call addStaff without a subsequent procedure --- .../db_helpers/{para.ts => case_manager.ts} | 30 +++++++++---- src/backend/routers/case_manager.ts | 42 +++++++++++++++---- src/backend/routers/para.ts | 38 ++--------------- src/pages/staff/index.tsx | 15 +------ 4 files changed, 63 insertions(+), 62 deletions(-) rename src/backend/lib/db_helpers/{para.ts => case_manager.ts} (84%) diff --git a/src/backend/lib/db_helpers/para.ts b/src/backend/lib/db_helpers/case_manager.ts similarity index 84% rename from src/backend/lib/db_helpers/para.ts rename to src/backend/lib/db_helpers/case_manager.ts index 463dec08..11a09028 100644 --- a/src/backend/lib/db_helpers/para.ts +++ b/src/backend/lib/db_helpers/case_manager.ts @@ -11,9 +11,9 @@ interface paraInputProps { export async function createPara( para: paraInputProps, db: KyselyDatabaseInstance, - caseManagerName: string, - fromEmail: string, - toEmail: string, + case_manager_id: string, + from_email: string, + to_email: string, env: Env ) { const { first_name, last_name, email } = para; @@ -37,7 +37,13 @@ export async function createPara( .executeTakeFirst(); // promise, will not interfere with returning paraData - void sendInviteEmail(fromEmail, toEmail, first_name, caseManagerName, env); + void sendInviteEmail( + from_email, + to_email, + first_name, + case_manager_id, + env + ); } return paraData; @@ -59,7 +65,15 @@ export async function sendInviteEmail( }); } -// todo -// export async function assignParaToCaseManager() { -// -// } +export async function assignParaToCaseManager( + para_id: string, + case_manager_id: string, + db: KyselyDatabaseInstance +) { + await db + .insertInto("paras_assigned_to_case_manager") + .values({ case_manager_id, para_id }) + .execute(); + + return; +} diff --git a/src/backend/routers/case_manager.ts b/src/backend/routers/case_manager.ts index d739a523..ad3c6f7d 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({ /** @@ -155,6 +159,31 @@ export const case_manager = router({ return result; }), + 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 + ); + }), + addPara: authenticatedProcedure .input( z.object({ @@ -162,13 +191,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 e717460a..18a7320f 100644 --- a/src/backend/routers/para.ts +++ b/src/backend/routers/para.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { getTransporter } from "../lib/nodemailer"; import { authenticatedProcedure, router } from "../trpc"; -import { createPara } from "../lib/db_helpers/para"; +import { createPara } from "../lib/db_helpers/case_manager"; export const para = router({ getParaById: authenticatedProcedure @@ -54,39 +54,9 @@ export const para = router({ return para; - 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(); - - // 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:

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 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 ec04193f..c66e8868 100644 --- a/src/pages/staff/index.tsx +++ b/src/pages/staff/index.tsx @@ -7,18 +7,7 @@ const Staff = () => { const { data: paras, isLoading } = trpc.case_manager.getMyParas.useQuery(); const { data: me } = trpc.user.getMe.useQuery(); - // const addPara = trpc.para.addPara.useMutation({}) - - const createPara = trpc.para.createPara.useMutation({ - onSuccess: async (data) => { - console.log(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(), }); @@ -27,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, From 138a120c9db244813a2af79d9e6be32cf19636ec Mon Sep 17 00:00:00 2001 From: Connor Wales Date: Sun, 7 Apr 2024 14:31:32 -0700 Subject: [PATCH 3/6] add return types to db_helpers/case_managers functions, comments to db_helpers/case_manager, routers/case_manager, routers/para --- src/backend/lib/db_helpers/case_manager.ts | 21 ++++++++++++++++++--- src/backend/routers/case_manager.ts | 7 +++++++ src/backend/routers/para.ts | 3 +++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/backend/lib/db_helpers/case_manager.ts b/src/backend/lib/db_helpers/case_manager.ts index 11a09028..6073ece3 100644 --- a/src/backend/lib/db_helpers/case_manager.ts +++ b/src/backend/lib/db_helpers/case_manager.ts @@ -1,6 +1,7 @@ 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; @@ -8,6 +9,11 @@ interface paraInputProps { 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, @@ -15,7 +21,7 @@ export async function createPara( from_email: string, to_email: string, env: Env -) { +): Promise { const { first_name, last_name, email } = para; let paraData = await db @@ -49,13 +55,16 @@ export async function createPara( 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, @@ -63,13 +72,19 @@ export async function sendInviteEmail( 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

`, }); + 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 }) diff --git a/src/backend/routers/case_manager.ts b/src/backend/routers/case_manager.ts index ad3c6f7d..f74f5f44 100644 --- a/src/backend/routers/case_manager.ts +++ b/src/backend/routers/case_manager.ts @@ -159,6 +159,10 @@ 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({ @@ -184,6 +188,9 @@ export const case_manager = router({ ); }), + /** + * Deprecated: use addStaff instead + */ addPara: authenticatedProcedure .input( z.object({ diff --git a/src/backend/routers/para.ts b/src/backend/routers/para.ts index 18a7320f..be06935c 100644 --- a/src/backend/routers/para.ts +++ b/src/backend/routers/para.ts @@ -32,6 +32,9 @@ export const para = router({ return result; }), + /** + * Deprecated: use case_manager.addStaff instead + */ createPara: authenticatedProcedure .input( z.object({ From 42ce373cc588e35db458f826025d4f63882c7b76 Mon Sep 17 00:00:00 2001 From: Connor Wales Date: Sun, 7 Apr 2024 14:48:36 -0700 Subject: [PATCH 4/6] add test for 'addStaff' procedure to routers/case_manager.test --- src/backend/routers/case_manager.test.ts | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/backend/routers/case_manager.test.ts b/src/backend/routers/case_manager.test.ts index 96fb9373..58d7929f 100644 --- a/src/backend/routers/case_manager.test.ts +++ b/src/backend/routers/case_manager.test.ts @@ -1,5 +1,6 @@ import test from "ava"; import { getTestServer } from "@/backend/tests"; +import { constants } from "fs"; test("getMyStudents", async (t) => { const { trpc, db, seed } = await getTestServer(t, { @@ -200,6 +201,31 @@ test("getMyParas", async (t) => { t.is(myParas.length, 1); }); +test("addStaff", async (t) => { + const { trpc, seed } = 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", From 4281a3250c09c1ff55cd741c6c8b5bd54826c690 Mon Sep 17 00:00:00 2001 From: Connor Wales Date: Sun, 7 Apr 2024 22:06:29 -0700 Subject: [PATCH 5/6] remove unnecessary import from routers/case_manager.test --- src/backend/routers/case_manager.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/routers/case_manager.test.ts b/src/backend/routers/case_manager.test.ts index 58d7929f..eeb80399 100644 --- a/src/backend/routers/case_manager.test.ts +++ b/src/backend/routers/case_manager.test.ts @@ -1,6 +1,5 @@ import test from "ava"; import { getTestServer } from "@/backend/tests"; -import { constants } from "fs"; test("getMyStudents", async (t) => { const { trpc, db, seed } = await getTestServer(t, { From 175a9525d7af7ce143e7923cec00f42fe1b23289 Mon Sep 17 00:00:00 2001 From: Connor Wales Date: Mon, 22 Apr 2024 15:04:29 -0700 Subject: [PATCH 6/6] remove unused 'seed' from routers/case_manager.test.ts --- src/backend/routers/case_manager.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/routers/case_manager.test.ts b/src/backend/routers/case_manager.test.ts index eeb80399..870ebf5d 100644 --- a/src/backend/routers/case_manager.test.ts +++ b/src/backend/routers/case_manager.test.ts @@ -201,7 +201,7 @@ test("getMyParas", async (t) => { }); test("addStaff", async (t) => { - const { trpc, seed } = await getTestServer(t, { + const { trpc } = await getTestServer(t, { authenticateAs: "case_manager", });