Skip to content

Commit

Permalink
feat: edit users
Browse files Browse the repository at this point in the history
  • Loading branch information
thomhickey committed Nov 15, 2024
1 parent 0ff2db0 commit 726b5c5
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 9 deletions.
52 changes: 52 additions & 0 deletions src/backend/routers/user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { hasAuthenticated, hasAdmin, router } from "../trpc";
import { z } from "zod";
import { UserType, ROLE_OPTIONS } from "@/types/auth";

const sortOrderSchema = z.enum(["asc", "desc"]).default("asc");
const sortBySchema = z
Expand All @@ -23,6 +24,8 @@ const createUserSchema = z.object({
.transform((role) => role.toLowerCase()),
});

const roleValues = ROLE_OPTIONS.map((r) => r.value) as [string, ...string[]];

export const user = router({
getMe: hasAuthenticated.query(async (req) => {
const { userId } = req.ctx.auth;
Expand Down Expand Up @@ -143,4 +146,53 @@ export const user = router({

return user;
}),

getUserById: hasAdmin
.input(z.object({ user_id: z.string() }))
.query(async (req) => {
const { user_id } = req.input;

return await req.ctx.db
.selectFrom("user")
.selectAll()
.where("user_id", "=", user_id)
.executeTakeFirstOrThrow();
}),

editUser: hasAdmin
.input(
z.object({
user_id: z.string(),
first_name: z.string(),
last_name: z.string(),
email: z.string().email(),
role: z.enum(roleValues).transform((role) => {
switch (role) {
case "ADMIN":
return UserType.Admin;
case "CASE_MANAGER":
return UserType.CaseManager;
case "PARA":
return UserType.Para;
default:
return UserType.User;
}
}),
})
)
.mutation(async (req) => {
const { user_id, first_name, last_name, email, role } = req.input;

return await req.ctx.db
.updateTable("user")
.set({
first_name,
last_name,
email: email.toLowerCase(),
role,
})
.where("user_id", "=", user_id)
.returningAll()
.executeTakeFirstOrThrow();
}),
});
5 changes: 4 additions & 1 deletion src/components/table/table2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface Column<T extends BaseEntity> {
value: T[keyof T] | undefined,
onChange: (value: T[keyof T]) => void
) => React.ReactNode;
renderCell?: (value: T[keyof T]) => React.ReactNode;
}

interface TableProps<T extends BaseEntity> {
Expand Down Expand Up @@ -215,7 +216,9 @@ export function Table2<T extends BaseEntity>({
>
{columns.map((column) => (
<TableCell key={column.id.toString()}>
{row[column.id]}
{column.renderCell
? column.renderCell(row[column.id])
: row[column.id]}
</TableCell>
))}
</StyledTableRow>
Expand Down
13 changes: 5 additions & 8 deletions src/pages/admin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { trpc } from "@/client/lib/trpc";
import { Table2, Column, BaseEntity } from "@/components/table/table2";
import { useRouter } from "next/router";
import { useState } from "react";
import { ROLE_OPTIONS } from "@/types/auth";
import {
TextField,
FormControl,
Expand All @@ -11,18 +12,13 @@ import {
MenuItem,
} from "@mui/material";
import { SelectChangeEvent } from "@mui/material/Select";
import { getRoleLabel } from "@/types/auth";

interface User extends BaseEntity {
user_id: string;
role: string;
}

const ROLE_OPTIONS = [
{ label: "Para", value: "PARA" },
{ label: "Case Manager", value: "CASE_MANAGER" },
{ label: "Admin", value: "ADMIN" },
] as const;

const AdminHome: React.FC = () => {
const utils = trpc.useContext();
const router = useRouter();
Expand Down Expand Up @@ -109,11 +105,12 @@ const AdminHome: React.FC = () => {
{
id: "role",
label: "Role",
renderCell: (value) => getRoleLabel(value as string),
renderInput: (value, onChange) => (
<FormControl size="small">
<InputLabel>Role</InputLabel>
<Select
value={(value as (typeof ROLE_OPTIONS)[number]["value"]) || ""}
value={(value as string)?.toUpperCase() || ""}
label="Role"
onChange={(e: SelectChangeEvent) => onChange(e.target.value)}
sx={{ minWidth: 120 }}
Expand All @@ -135,7 +132,7 @@ const AdminHome: React.FC = () => {
];

const handleRowClick = async (user: User) => {
await router.push(`/staff/${user.user_id}`);
await router.push(`/users/${user.user_id}`);
};

return (
Expand Down
158 changes: 158 additions & 0 deletions src/pages/users/[user_id].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { trpc } from "@/client/lib/trpc";
import { Box, Button, Container, Modal, Stack } from "@mui/material";
import { useRouter } from "next/router";
import { useState } from "react";
import { UserType, ROLE_OPTIONS } from "@/types/auth";
import $CompassModal from "@/components/design_system/modal/CompassModal.module.css";
import $button from "@/components/design_system/button/Button.module.css";
import $Form from "@/styles/Form.module.css";
import $input from "@/styles/Input.module.css";
import { getRoleLabel } from "@/types/auth";

interface UserFormData {
first_name: string;
last_name: string;
email: string;
role: UserType;
}

const ViewUserPage = () => {
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);

const utils = trpc.useContext();
const router = useRouter();
const { user_id } = router.query;

const { data: user, isLoading } = trpc.user.getUserById.useQuery(
{ user_id: user_id as string },
{
enabled: Boolean(user_id),
retry: false,
onError: () => returnToUserList(),
}
);

const returnToUserList = async () => {
await router.push(`/admin`);
};

const editMutation = trpc.user.editUser.useMutation({
onSuccess: () => utils.user.getUserById.invalidate(),
});

const handleEditUser = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);

if (!user) return;

const role = formData.get("role") as string;
const userData: UserFormData = {
first_name: formData.get("firstName") as string,
last_name: formData.get("lastName") as string,
email: formData.get("email") as string,
role: role as UserType,
};

editMutation.mutate({
user_id: user.user_id,
...userData,
});

handleClose();
};

if (isLoading) return <div>Loading...</div>;
if (!user) return null;

return (
<Stack spacing={2}>
<Container>
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 2 }}>
<h1>
{user.first_name} {user.last_name}
</h1>
<Button onClick={handleOpen}>Edit User</Button>
</Box>

<Box sx={{ display: "grid", gap: 2 }}>
<div>
<strong>Email:</strong> {user.email}
</div>
<div>
<strong>Role:</strong> {getRoleLabel(user.role)}
</div>
</Box>

<Modal
open={open}
onClose={handleClose}
aria-labelledby="edit-user-modal"
>
<Box className={$CompassModal.modalContent}>
<h2>Edit User</h2>
<form onSubmit={handleEditUser} className={$Form.formPadding}>
<div className={$input.default}>
<input
type="text"
name="firstName"
defaultValue={user.first_name}
placeholder="First Name"
required
/>
<input
type="text"
name="lastName"
defaultValue={user.last_name}
placeholder="Last Name"
required
/>
<input
type="email"
name="email"
defaultValue={user.email}
placeholder="Email"
required
/>
<select
name="role"
defaultValue={user.role.toUpperCase()}
required
>
{ROLE_OPTIONS.map(({ value, label }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
<Box
sx={{
display: "flex",
gap: 2,
justifyContent: "flex-end",
mt: 2,
}}
>
<button type="submit" className={$button.default}>
Save Changes
</button>
<button
type="button"
onClick={handleClose}
className={$button.default}
>
Cancel
</button>
</Box>
</form>
</Box>
</Modal>
</Container>
</Stack>
);
};

export default ViewUserPage;
13 changes: 13 additions & 0 deletions src/types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,16 @@ export enum UserType {
CaseManager = "case_manager",
Admin = "admin",
}

export const ROLE_OPTIONS = [
{ label: "Para", value: "PARA" },
{ label: "Case Manager", value: "CASE_MANAGER" },
{ label: "Admin", value: "ADMIN" },
] as const;

export function getRoleLabel(role: string): string {
const option = ROLE_OPTIONS.find(
(opt) => opt.value.toLowerCase() === role.toLowerCase()
);
return option?.label || role;
}

0 comments on commit 726b5c5

Please sign in to comment.