Skip to content

Commit 28ef8de

Browse files
authored
[#509] Clean up and consolidate data table implementation across pages
* Create new generic DataTable component, update Students page implementation, remove email from students table schema * Debounce search value, implement search in backend * Implement sort in back-end, use query params to manage state * Navigate on table row click * Change students migration from drop to alter * Restore email as optional * Upgrade zod. Create IEP with student when end_date provided. Allow override of global error handler. Show error states on student form. * Make sure Add Student works for first record, disable button when form visible * Migrate Staff page to new table implementation * More refactor of data table * Delete old table implementation * Restore case manager logic around emails, fix tests * Handle creating IEP with end date for student * Make email optional for student * When changing DataTable sort column, default to sort ascending
1 parent 3f3c043 commit 28ef8de

26 files changed

+1070
-1106
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"tsx": "^4.19.2",
6464
"winston": "^3.17.0",
6565
"zapatos": "^6.4.4",
66-
"zod": "^3.23.8"
66+
"zod": "^3.25.28"
6767
},
6868
"devDependencies": {
6969
"@ava/get-port": "^2.0.0",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE student ALTER COLUMN email DROP NOT NULL;
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
{
2-
"ignorePatterns": ["*"]
3-
}
2+
"ignorePatterns": [
3+
"*"
4+
]
5+
}

src/backend/db/zapatos/schema.d.ts

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/backend/lib/db_helpers/case_manager.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export async function assignParaToCaseManager(
101101
type createStudentProps = {
102102
first_name: string;
103103
last_name: string;
104-
email: string;
104+
email?: string | null | undefined;
105105
grade: number;
106106
db: KyselyDatabaseInstance;
107107
userId: string;
@@ -127,32 +127,34 @@ export async function createAndAssignStudent({
127127
db,
128128
userId,
129129
}: createStudentProps) {
130-
const lookahead = await db
131-
.selectFrom("student")
132-
.selectAll()
133-
.where("email", "=", email)
134-
.execute();
130+
if (email) {
131+
const lookahead = await db
132+
.selectFrom("student")
133+
.selectAll()
134+
.where("email", "=", email)
135+
.execute();
135136

136-
if (lookahead.length > 0) {
137-
const student = lookahead[0];
138-
if (student.assigned_case_manager_id === userId) {
139-
throw STUDENT_ASSIGNED_TO_YOU_ERR;
137+
if (lookahead.length > 0) {
138+
const student = lookahead[0];
139+
if (student.assigned_case_manager_id === userId) {
140+
throw STUDENT_ASSIGNED_TO_YOU_ERR;
141+
}
142+
// not null
143+
else if (student.assigned_case_manager_id) {
144+
throw STUDENT_ALREADY_ASSIGNED_ERR;
145+
}
146+
// if student exists in table, but is unassigned,
147+
// handle in onConflict during creation
140148
}
141-
// not null
142-
else if (student.assigned_case_manager_id) {
143-
throw STUDENT_ALREADY_ASSIGNED_ERR;
144-
}
145-
// if student exists in table, but is unassigned,
146-
// handle in onConflict during creation
147149
}
148150

149151
// else, safe to create or re-assign student
150-
await db
152+
return db
151153
.insertInto("student")
152154
.values({
153155
first_name,
154156
last_name,
155-
email: email.toLowerCase(),
157+
email: email?.toLowerCase(),
156158
assigned_case_manager_id: userId,
157159
grade,
158160
})

src/backend/routers/case_manager.ts

Lines changed: 135 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { z } from "zod";
2+
import { parseISO, sub } from "date-fns";
3+
24
import { hasCaseManager, router } from "../trpc";
35
import {
46
createPara,
@@ -22,28 +24,64 @@ export const case_manager = router({
2224
return result;
2325
}),
2426

25-
getMyStudentsAndIepInfo: hasCaseManager.query(async (req) => {
26-
const { userId } = req.ctx.auth;
27+
getMyStudentsAndIepInfo: hasCaseManager
28+
.input(
29+
z
30+
.object({
31+
search: z.string().optional(),
32+
sort: z.string().optional(),
33+
sortAsc: z.coerce.boolean().optional(),
34+
})
35+
.optional()
36+
)
37+
.query(async (req) => {
38+
const { userId } = req.ctx.auth;
39+
const { search, sort, sortAsc = true } = req.input ?? {};
2740

28-
const studentData = await req.ctx.db
29-
.selectFrom("iep")
30-
.fullJoin("student", (join) =>
31-
join.onRef("student.student_id", "=", "iep.student_id")
32-
)
33-
.where("assigned_case_manager_id", "=", userId)
34-
.select([
35-
"student.student_id as student_id",
36-
"first_name",
37-
"last_name",
38-
"student.email",
39-
"iep.iep_id as iep_id",
40-
"iep.end_date as end_date",
41-
"student.grade as grade",
42-
])
43-
.execute();
41+
let query = req.ctx.db
42+
.selectFrom("iep")
43+
.fullJoin("student", (join) =>
44+
join.onRef("student.student_id", "=", "iep.student_id")
45+
)
46+
.where("assigned_case_manager_id", "=", userId);
4447

45-
return studentData;
46-
}),
48+
if (search) {
49+
query = query.where((eb) =>
50+
eb.or([
51+
eb("student.first_name", "ilike", `%${search}%`),
52+
eb("student.last_name", "ilike", `%${search}%`),
53+
])
54+
);
55+
}
56+
57+
const result = await query
58+
.select([
59+
"student.student_id as student_id",
60+
"first_name",
61+
"last_name",
62+
"student.grade as grade",
63+
"iep.iep_id as iep_id",
64+
"iep.end_date as end_date",
65+
])
66+
.orderBy(
67+
(eb) => {
68+
switch (sort) {
69+
case "last_name":
70+
return eb.ref("student.last_name");
71+
case "grade":
72+
return eb.ref("student.grade");
73+
case "end_date":
74+
return eb.ref("iep.end_date");
75+
default:
76+
return eb.ref("student.first_name");
77+
}
78+
},
79+
sortAsc ? "asc" : "desc"
80+
)
81+
.execute();
82+
83+
return result;
84+
}),
4785

4886
/**
4987
* Adds the given student to the CM's roster. The student row is created if
@@ -55,17 +93,37 @@ export const case_manager = router({
5593
z.object({
5694
first_name: z.string(),
5795
last_name: z.string(),
58-
email: z.string().email(),
96+
email: z.string().email().nullable().optional(),
5997
grade: z.number(),
98+
end_date: z.string().date().optional(),
6099
})
61100
)
62101
.mutation(async (req) => {
63102
const { userId } = req.ctx.auth;
64103

65-
await createAndAssignStudent({
66-
...req.input,
67-
userId,
68-
db: req.ctx.db,
104+
return req.ctx.db.transaction().execute(async (trx) => {
105+
const result = await createAndAssignStudent({
106+
...req.input,
107+
userId,
108+
db: trx,
109+
});
110+
111+
const { end_date } = req.input;
112+
if (end_date) {
113+
const start_date = sub(parseISO(end_date), { years: 1 });
114+
await trx
115+
.insertInto("iep")
116+
.values({
117+
student_id: result.student_id,
118+
case_manager_id: userId,
119+
start_date,
120+
end_date: parseISO(end_date),
121+
})
122+
.returningAll()
123+
.executeTakeFirstOrThrow();
124+
}
125+
126+
return result;
69127
});
70128
}),
71129

@@ -78,7 +136,7 @@ export const case_manager = router({
78136
student_id: z.string(),
79137
first_name: z.string(),
80138
last_name: z.string(),
81-
email: z.string().email(),
139+
email: z.string().email().nullable().optional(),
82140
grade: z.number(),
83141
})
84142
)
@@ -104,7 +162,7 @@ export const case_manager = router({
104162
.set({
105163
first_name,
106164
last_name,
107-
email: email.toLowerCase(),
165+
email: email?.toLowerCase(),
108166
grade,
109167
})
110168
.where("student_id", "=", student_id)
@@ -131,22 +189,58 @@ export const case_manager = router({
131189
.execute();
132190
}),
133191

134-
getMyParas: hasCaseManager.query(async (req) => {
135-
const { userId } = req.ctx.auth;
192+
getMyParas: hasCaseManager
193+
.input(
194+
z
195+
.object({
196+
search: z.string().optional(),
197+
sort: z.string().optional(),
198+
sortAsc: z.coerce.boolean().optional(),
199+
})
200+
.optional()
201+
)
202+
.query(async (req) => {
203+
const { userId } = req.ctx.auth;
204+
const { search, sort, sortAsc = true } = req.input ?? {};
136205

137-
const result = await req.ctx.db
138-
.selectFrom("user")
139-
.innerJoin(
140-
"paras_assigned_to_case_manager",
141-
"user.user_id",
142-
"paras_assigned_to_case_manager.para_id"
143-
)
144-
.where("paras_assigned_to_case_manager.case_manager_id", "=", userId)
145-
.selectAll()
146-
.execute();
206+
let query = req.ctx.db
207+
.selectFrom("user")
208+
.innerJoin(
209+
"paras_assigned_to_case_manager",
210+
"user.user_id",
211+
"paras_assigned_to_case_manager.para_id"
212+
)
213+
.where("paras_assigned_to_case_manager.case_manager_id", "=", userId);
147214

148-
return result;
149-
}),
215+
if (search) {
216+
query = query.where((eb) =>
217+
eb.or([
218+
eb("user.first_name", "ilike", `%${search}%`),
219+
eb("user.last_name", "ilike", `%${search}%`),
220+
eb("user.email", "ilike", `%${search}%`),
221+
])
222+
);
223+
}
224+
225+
const result = await query
226+
.selectAll()
227+
.orderBy(
228+
(eb) => {
229+
switch (sort) {
230+
case "last_name":
231+
return eb.ref("user.last_name");
232+
case "email":
233+
return eb.ref("user.email");
234+
default:
235+
return eb.ref("user.first_name");
236+
}
237+
},
238+
sortAsc ? "asc" : "desc"
239+
)
240+
.execute();
241+
242+
return result;
243+
}),
150244

151245
/**
152246
* Handles creation of para and assignment to user, attempts to send

0 commit comments

Comments
 (0)