Skip to content

Commit

Permalink
feat: adding users
Browse files Browse the repository at this point in the history
  • Loading branch information
thomhickey committed Nov 15, 2024
1 parent cd52ebc commit 38c89c2
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 11 deletions.
37 changes: 37 additions & 0 deletions src/backend/routers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ const paginationInput = z.object({
search: z.string().optional(),
});

const createUserSchema = z.object({
first_name: z.string(),
last_name: z.string(),
email: z.string().email(),
role: z
.enum(["ADMIN", "CASE_MANAGER", "PARA"])
.transform((role) => role.toLowerCase()),
});

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

return result.length > 0;
}),

createUser: hasAdmin.input(createUserSchema).mutation(async (req) => {
const { first_name, last_name, email, role } = req.input;

// Check if user already exists
const existingUser = await req.ctx.db
.selectFrom("user")
.where("email", "=", email)
.selectAll()
.executeTakeFirst();

if (existingUser) {
throw new Error("User with this email already exists");
}

const user = await req.ctx.db
.insertInto("user")
.values({
first_name,
last_name,
email,
role,
})
.returningAll()
.executeTakeFirstOrThrow();

return user;
}),
});
82 changes: 79 additions & 3 deletions src/components/table/table2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ export interface BaseEntity {
export interface Column<T> {
id: keyof T;
label: string;
hasInput?: boolean;
renderInput?: (value: any, onChange: (value: any) => void) => React.ReactNode;
}

interface TableProps<T extends BaseEntity> {
data: T[];
columns: Column<T>[];
type: "Students" | "Staff" | "Users";
type: string;
onRowClick?: (row: T) => void;
page?: number;
totalPages?: number;
Expand All @@ -43,6 +43,8 @@ interface TableProps<T extends BaseEntity> {
onSort: (sortBy: keyof T, sortOrder: "asc" | "desc") => void;
onSearch?: (search: string) => void;
searchTerm?: string;
onAdd?: (data: Omit<T, "id">) => Promise<void>;
showAddRow?: boolean;
}

const StyledTableRow = styled(TableRow)(() => ({
Expand All @@ -68,8 +70,12 @@ export function Table2<T extends BaseEntity>({
onSort,
onSearch,
searchTerm = "",
onAdd,
showAddRow = false,
}: TableProps<T>) {
const [localSearchTerm, setLocalSearchTerm] = useState(searchTerm);
const [newRowData, setNewRowData] = useState<Partial<T>>({});
const [isAddingRow, setIsAddingRow] = useState(false);

const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
Expand All @@ -83,10 +89,30 @@ export function Table2<T extends BaseEntity>({
onSort(property, isAsc ? "desc" : "asc");
};

const handleAddRow = async (e: React.FormEvent) => {
e.preventDefault();
if (onAdd) {
try {
await onAdd(newRowData as Omit<T, "id">);
setNewRowData({});
setIsAddingRow(false);
} catch (error) {
console.error(error);
}
}
};

return (
<Box sx={{ width: "100%" }}>
<Box sx={{ mb: 2, display: "flex", justifyContent: "space-between" }}>
<h3>{type}</h3>
<Box sx={{ display: "flex", gap: 2, alignItems: "center" }}>
<h3>{type}</h3>
{onAdd && !isAddingRow && showAddRow && (
<Button variant="contained" onClick={() => setIsAddingRow(true)}>
Add {type.slice(0, -1)}
</Button>
)}
</Box>
<TextField
size="small"
placeholder="Search..."
Expand Down Expand Up @@ -129,6 +155,56 @@ export function Table2<T extends BaseEntity>({
</TableRow>
</TableHead>
<TableBody>
{isAddingRow && (
<TableRow>
<TableCell colSpan={columns.length + 1}>
<form onSubmit={handleAddRow}>
<Box
sx={{ display: "flex", gap: 2, alignItems: "flex-end" }}
>
{columns.map((column) => (
<Box key={column.id.toString()}>
{column.renderInput?.(
newRowData[column.id],
(value: T[keyof T]) =>
setNewRowData((prev) => ({
...prev,
[column.id]: value,
}))
) ?? (
<TextField
size="small"
label={column.label}
value={newRowData[column.id] || ""}
onChange={(e) =>
setNewRowData((prev) => ({
...prev,
[column.id]: e.target.value,
}))
}
/>
)}
</Box>
))}
<Box sx={{ display: "flex", gap: 1 }}>
<Button type="submit" variant="contained" size="small">
Add
</Button>
<Button
onClick={() => {
setIsAddingRow(false);
setNewRowData({});
}}
size="small"
>
Cancel
</Button>
</Box>
</Box>
</form>
</TableCell>
</TableRow>
)}
{data.map((row, index) => (
<StyledTableRow
key={row.id || index}
Expand Down
107 changes: 99 additions & 8 deletions src/pages/admin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,135 @@ import { trpc } from "@/client/lib/trpc";
import { Table2, Column, BaseEntity } from "@/components/table/table2";
import { useRouter } from "next/router";
import { useState } from "react";
import {
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
} from "@mui/material";
import { SelectChangeEvent } from "@mui/material/Select";

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();
const [page, setPage] = useState(1);
const [sortBy, setSortBy] = useState<keyof User>("first_name");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [searchTerm] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const [searchTerm, setSearchTerm] = useState("");
const pageSize = 10;

const { data, isLoading } = trpc.user.getUsers.useQuery({
page,
pageSize,
sortBy,

Check failure on line 38 in src/pages/admin/index.tsx

View workflow job for this annotation

GitHub Actions / type-check

Type 'keyof User' is not assignable to type '"role" | "first_name" | "last_name" | "email" | undefined'.
sortOrder,
search: debouncedSearchTerm,
search: searchTerm,
});

const createUserMutation = trpc.user.createUser.useMutation({
onSuccess: async () => {
await utils.user.getUsers.invalidate();
},
});

const handleAddUser = async (userData: Omit<User, "id">) => {
try {
await createUserMutation.mutateAsync({
...userData,
role: userData.role || "PARA", // Set default role if needed

Check failure on line 53 in src/pages/admin/index.tsx

View workflow job for this annotation

GitHub Actions / type-check

Type 'string | number' is not assignable to type '"ADMIN" | "CASE_MANAGER" | "PARA"'.
});
} catch (error) {
console.error(error);
}
};

const handleSort = (newSortBy: keyof User, newSortOrder: "asc" | "desc") => {
setSortBy(newSortBy);
setSortOrder(newSortOrder);
};

const handleSearch = (search: string) => {
setDebouncedSearchTerm(search);
setSearchTerm(search);
setPage(1); // Reset to first page when searching
};

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

const columns: Column<User>[] = [
{ id: "first_name", label: "First Name" },
{ id: "last_name", label: "Last Name" },
{ id: "email", label: "Email" },
{ id: "role", label: "Role" },
{
id: "first_name",
label: "First Name",
renderInput: (value, onChange) => (
<TextField
size="small"
label="First Name"
value={(value as string) || ""}
onChange={(e) => onChange(e.target.value)}
/>
),
},
{
id: "last_name",
label: "Last Name",
renderInput: (value, onChange) => (
<TextField
size="small"
label="Last Name"
value={(value as string) || ""}
onChange={(e) => onChange(e.target.value)}
/>
),
},
{
id: "email",
label: "Email",
renderInput: (value, onChange) => (
<TextField
size="small"
label="Email"
value={(value as string) || ""}
onChange={(e) => onChange(e.target.value)}
/>
),
},
{
id: "role",
label: "Role",
renderInput: (value, onChange) => (
<FormControl size="small">
<InputLabel>Role</InputLabel>
<Select
value={(value as (typeof ROLE_OPTIONS)[number]["value"]) || ""}
label="Role"
onChange={(e: SelectChangeEvent) => onChange(e.target.value)}
sx={{ minWidth: 120 }}
MenuProps={{
PaperProps: {
elevation: 1,
},
}}
>
{ROLE_OPTIONS.map(({ label, value }) => (
<MenuItem key={value} value={value}>
{label}
</MenuItem>
))}
</Select>
</FormControl>
),
},
];

const handleRowClick = async (user: User) => {
Expand All @@ -65,6 +154,8 @@ const AdminHome: React.FC = () => {
onSort={handleSort}
onSearch={handleSearch}
searchTerm={searchTerm}
onAdd={handleAddUser}
showAddRow={true}
/>
</div>
);
Expand Down

0 comments on commit 38c89c2

Please sign in to comment.