mirror of
https://github.com/aaronleetw/savage-tracking.git
synced 2024-11-14 19:11:39 -08:00
completed admin & rfid pages
This commit is contained in:
parent
af65bb7191
commit
54cdb7e83f
16 changed files with 882 additions and 63 deletions
|
@ -13,3 +13,4 @@
|
||||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||||
DATABASE_URL="file:./db.sqlite"
|
DATABASE_URL="file:./db.sqlite"
|
||||||
JWT_SECRET="well, now the secret is spoiled, isn't it?"
|
JWT_SECRET="well, now the secret is spoiled, isn't it?"
|
||||||
|
TZ="Asia/Taipei"
|
|
@ -17,7 +17,9 @@ model User {
|
||||||
name String
|
name String
|
||||||
username String @unique
|
username String @unique
|
||||||
password String
|
password String
|
||||||
|
rfid String? @unique
|
||||||
isAdmin Boolean @default(false)
|
isAdmin Boolean @default(false)
|
||||||
|
rosterOnly Boolean @default(false)
|
||||||
|
|
||||||
periods Period[]
|
periods Period[]
|
||||||
|
|
||||||
|
@ -31,6 +33,10 @@ model User {
|
||||||
model TimePeriod {
|
model TimePeriod {
|
||||||
id Int @id
|
id Int @id
|
||||||
name String
|
name String
|
||||||
|
|
||||||
|
start String
|
||||||
|
end String
|
||||||
|
|
||||||
periods Period[]
|
periods Period[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
|
@ -14,6 +14,11 @@ export default function DashboardHeader({ url }: { url: string }) {
|
||||||
}
|
}
|
||||||
}, [logout.isSuccess])
|
}, [logout.isSuccess])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (session.data?.rosterOnly && !url.startsWith("/dash/admin/roster"))
|
||||||
|
push("/dash/admin/roster");
|
||||||
|
}, [session.data])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-3xl md:text-5xl font-bold bg-indigo-100">
|
<div className="text-3xl md:text-5xl font-bold bg-indigo-100">
|
||||||
<div className="p-5 pb-0">
|
<div className="p-5 pb-0">
|
||||||
|
@ -28,49 +33,57 @@ export default function DashboardHeader({ url }: { url: string }) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center w-full gap-3">
|
<div className="flex items-center w-full gap-3">
|
||||||
<Link href="/dash">
|
{
|
||||||
<button className={[
|
!session?.data?.rosterOnly && (<>
|
||||||
"text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-sky-200 focus:ring-opacity-70",
|
<Link href="/dash">
|
||||||
url == "/dash" ? "bg-sky-600 text-white" : "bg-sky-300 text-black"
|
<button className={[
|
||||||
].join(" ")}>
|
"text-sm md:text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-sky-200 focus:ring-opacity-70",
|
||||||
Time Selection
|
url == "/dash" ? "bg-sky-600 text-white" : "bg-sky-300 text-black"
|
||||||
</button>
|
].join(" ")}>
|
||||||
</Link>
|
Time Selection
|
||||||
<Link href="/dash/chgPassword">
|
</button>
|
||||||
<button className={[
|
</Link>
|
||||||
"text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-sky-200 focus:ring-opacity-70",
|
<Link href="/dash/chgPassword">
|
||||||
url == "/dash/chgPassword" ? "bg-sky-600 text-white" : "bg-sky-300 text-black"
|
<button className={[
|
||||||
].join(" ")}>
|
"text-sm md:text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-sky-200 focus:ring-opacity-70",
|
||||||
Change Password
|
url == "/dash/chgPassword" ? "bg-sky-600 text-white" : "bg-sky-300 text-black"
|
||||||
</button>
|
].join(" ")}>
|
||||||
</Link>
|
Change Password
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
{
|
{
|
||||||
session?.data?.isAdmin && (
|
session?.data?.isAdmin && (
|
||||||
<>
|
<>
|
||||||
<Link href="/dash/admin">
|
|
||||||
<button className={[
|
|
||||||
"text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-emerald-200 focus:ring-opacity-70",
|
|
||||||
url == "/dash/admin" ? "bg-emerald-600 text-white" : "bg-emerald-200 text-black"
|
|
||||||
].join(" ")}>
|
|
||||||
Admin
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
<Link href="/dash/admin/roster">
|
<Link href="/dash/admin/roster">
|
||||||
<button className={[
|
<button className={[
|
||||||
"text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-emerald-200 focus:ring-opacity-70",
|
"text-sm md:text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-emerald-200 focus:ring-opacity-70",
|
||||||
url == "/dash/admin/roster" ? "bg-emerald-600 text-white" : "bg-emerald-200 text-black"
|
url == "/dash/admin/roster" ? "bg-emerald-600 text-white" : "bg-emerald-200 text-black"
|
||||||
].join(" ")}>
|
].join(" ")}>
|
||||||
Print Roster
|
View Roster
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
<Link href="/dash/admin/rfid">
|
|
||||||
<button className={[
|
|
||||||
"text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-emerald-200 focus:ring-opacity-70",
|
|
||||||
url == "/dash/admin/rfid" ? "bg-emerald-600 text-white" : "bg-emerald-200 text-black"
|
|
||||||
].join(" ")}>
|
|
||||||
RFID Log
|
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
{
|
||||||
|
!session?.data?.rosterOnly && (<>
|
||||||
|
<Link href="/dash/admin">
|
||||||
|
<button className={[
|
||||||
|
"text-sm md:text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-emerald-200 focus:ring-opacity-70",
|
||||||
|
url == "/dash/admin" ? "bg-emerald-600 text-white" : "bg-emerald-200 text-black"
|
||||||
|
].join(" ")}>
|
||||||
|
Admin
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/dash/admin/rfid">
|
||||||
|
<button className={[
|
||||||
|
"text-sm md:text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-emerald-200 focus:ring-opacity-70",
|
||||||
|
url == "/dash/admin/rfid" ? "bg-emerald-600 text-white" : "bg-emerald-200 text-black"
|
||||||
|
].join(" ")}>
|
||||||
|
RFID Setup
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
79
src/components/admin/Config.tsx
Normal file
79
src/components/admin/Config.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { AddTimePeriodSchema } from "~/utils/types";
|
||||||
|
|
||||||
|
export default function Config() {
|
||||||
|
const timePeriods = api.admin.getTimePeriods.useQuery();
|
||||||
|
const addTimePeriod = api.admin.addTimePeriod.useMutation();
|
||||||
|
const deleteTimePeriod = api.admin.deleteTimePeriod.useMutation();
|
||||||
|
|
||||||
|
type AddTimePeriodSchemaType = z.infer<typeof AddTimePeriodSchema>;
|
||||||
|
const { register, handleSubmit, formState: { errors } } = useForm<AddTimePeriodSchemaType>({ resolver: zodResolver(AddTimePeriodSchema) });
|
||||||
|
const onSubmit: SubmitHandler<AddTimePeriodSchemaType> = async (data, event) => addTimePeriod.mutateAsync(data).then(() => {
|
||||||
|
event?.target.reset();
|
||||||
|
timePeriods.refetch();
|
||||||
|
}).catch((err) => console.log(err));
|
||||||
|
|
||||||
|
return (<div>
|
||||||
|
<section>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div>
|
||||||
|
<p className="text-xl font-bold mb-1">Current time periods:</p>
|
||||||
|
<table className="table-auto border-collapse border w-fit">
|
||||||
|
<thead>
|
||||||
|
<tr className="*:p-1 *:border">
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Start</th>
|
||||||
|
<th>End</th>
|
||||||
|
<th>Operation</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
timePeriods.data?.map((period) => (
|
||||||
|
<tr className="*:p-1 *:border" key={period.id}>
|
||||||
|
<td>{period.id}</td>
|
||||||
|
<td>{period.name}</td>
|
||||||
|
<td>{period.start}</td>
|
||||||
|
<td>{period.end}</td>
|
||||||
|
<td className="text-center">
|
||||||
|
<button className="p-1 px-2 bg-red-600 text-white rounded-lg" onClick={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
deleteTimePeriod.mutateAsync(period.id).then(() => timePeriods.refetch());
|
||||||
|
}} type="button">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<tr className="*:p-1 *:border">
|
||||||
|
<td>
|
||||||
|
<input type="number" className="mt-1 block w-16 rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-70" placeholder="ID" {...register("id")} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-70" placeholder="Name" {...register("name")} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-70" placeholder="Start" {...register("startTime")} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-70" placeholder="End" {...register("endTime")} />
|
||||||
|
</td>
|
||||||
|
<td className="text-center">
|
||||||
|
<button className="p-1 px-2 bg-emerald-600 text-white rounded-lg" type="submit">Add</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{errors.id && <p className="text-red-500">{errors.id.message}</p>}
|
||||||
|
{errors.startTime && <p className="text-red-500">{errors.startTime.message}</p>}
|
||||||
|
{errors.endTime && <p className="text-red-500">{errors.endTime.message}</p>}
|
||||||
|
{errors.name && <p className="text-red-500">{errors.name.message}</p>}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{addTimePeriod.isError && <p className="text-red-500">{addTimePeriod.error.message}</p>}
|
||||||
|
</div>)
|
||||||
|
}
|
93
src/components/admin/Periods.tsx
Normal file
93
src/components/admin/Periods.tsx
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { AddPeriods, AddTimePeriodSchema } from "~/utils/types";
|
||||||
|
|
||||||
|
export default function Periods() {
|
||||||
|
const timePeriods = api.admin.getTimePeriods.useQuery();
|
||||||
|
const periods = api.admin.getPeriods.useQuery();
|
||||||
|
const addPeriods = api.admin.addPeriods.useMutation();
|
||||||
|
const enablePeriod = api.admin.enablePeriod.useMutation();
|
||||||
|
const disablePeriod = api.admin.disablePeriod.useMutation();
|
||||||
|
type AddPeriodsType = z.infer<typeof AddPeriods>;
|
||||||
|
const { register, handleSubmit, formState: { errors } } = useForm<AddPeriodsType>({ resolver: zodResolver(AddPeriods) });
|
||||||
|
const onSubmit: SubmitHandler<AddPeriodsType> = async (data, event) => addPeriods.mutateAsync(data).then(() => {
|
||||||
|
event?.target.reset();
|
||||||
|
periods.refetch();
|
||||||
|
}).catch((err) => console.log(err));
|
||||||
|
|
||||||
|
let periodCnt = 0;
|
||||||
|
const [periodsDataId, setPeriodsDataId] = useState<number[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const periodsDataId: number[] = [];
|
||||||
|
timePeriods.data?.forEach((timePeriod) => {
|
||||||
|
periodsDataId.push(timePeriod.id);
|
||||||
|
})
|
||||||
|
setPeriodsDataId(periodsDataId);
|
||||||
|
}, [timePeriods.data])
|
||||||
|
|
||||||
|
return (<div>
|
||||||
|
<table className="table-auto border-collapse border-2 border-black w-fit mb-5">
|
||||||
|
<thead>
|
||||||
|
<tr className="*:p-1 *:border border-b-2 border-b-black">
|
||||||
|
<th>Date</th>
|
||||||
|
{
|
||||||
|
timePeriods.data?.map((timePeriods) => {
|
||||||
|
return (<th key={timePeriods.id}>{timePeriods.name} ({timePeriods.start} ~ {timePeriods.end})</th>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
Object.keys(periods.data || {}).map((date) => {
|
||||||
|
periodCnt = 0;
|
||||||
|
return (
|
||||||
|
<tr className="*:p-1 *:border" key={date}>
|
||||||
|
<td>{date}</td>
|
||||||
|
{
|
||||||
|
timePeriods.data?.map((timePeriod) => {
|
||||||
|
const thisPeriodCnt = periodCnt;
|
||||||
|
if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) {
|
||||||
|
periodCnt++;
|
||||||
|
return (<td key={timePeriod.id * periodCnt} className="bg-emerald-600 text-white hover:cursor-pointer"
|
||||||
|
onClick={() => disablePeriod.mutateAsync(periods.data![date]![thisPeriodCnt]!.id!).then(() => periods.refetch())}>
|
||||||
|
Disable
|
||||||
|
</td>)
|
||||||
|
} else {
|
||||||
|
return (<td key={timePeriod.id * periodCnt} className="bg-yellow-600 text-white hover:cursor-pointer" onClick={() => enablePeriod.mutateAsync({
|
||||||
|
date: date,
|
||||||
|
timePeriodId: periodsDataId[thisPeriodCnt]!
|
||||||
|
}).then(() => periods.refetch())}>
|
||||||
|
Enable
|
||||||
|
</td>)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<hr />
|
||||||
|
<h2 className="text-xl font-bold mt-5">Add Date</h2>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="mb-2">
|
||||||
|
<label className="block text-md font-medium text-gray-700 mt-2">Date</label>
|
||||||
|
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-70" {...register("date")} />
|
||||||
|
{errors.date && <span className="text-red-500">{errors.date.message}</span>}
|
||||||
|
</div>
|
||||||
|
<button className="block w-fit h-fit mt-9 bg-emerald-600 px-3 py-2 rounded text-white focus:ring focus:ring-emerald-200 focus:ring-opacity-70 disabled:bg-emerald-400 mb-5" disabled={addPeriods.isLoading}>
|
||||||
|
Create Periods
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{addPeriods.error && <p className="text-red-500 mb-2">{addPeriods.error.message}</p>}
|
||||||
|
{addPeriods.isSuccess && <p className="text-green-500 mb-2">Add periods success!</p>}
|
||||||
|
</form>
|
||||||
|
</div>)
|
||||||
|
}
|
107
src/components/admin/Users.tsx
Normal file
107
src/components/admin/Users.tsx
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { AddTimePeriodSchema, AddUserSchema } from "~/utils/types";
|
||||||
|
|
||||||
|
export default function Users() {
|
||||||
|
const users = api.admin.getAllUsers.useQuery();
|
||||||
|
const addUser = api.admin.addUser.useMutation();
|
||||||
|
const resetPassword = api.admin.resetPassword.useMutation();
|
||||||
|
const toggleAdmin = api.admin.toggleUserIsAdmin.useMutation();
|
||||||
|
const toggleRosterOnly = api.admin.toggleUserRosterOnly.useMutation();
|
||||||
|
|
||||||
|
type AddUserSchemaType = z.infer<typeof AddUserSchema>;
|
||||||
|
const { register, handleSubmit, formState: { errors } } = useForm<AddUserSchemaType>({ resolver: zodResolver(AddUserSchema) });
|
||||||
|
const onSubmit: SubmitHandler<AddUserSchemaType> = async (data, event) => addUser.mutateAsync(data).then(() => {
|
||||||
|
event?.target.reset();
|
||||||
|
users.refetch();
|
||||||
|
}).catch((err) => console.log(err));
|
||||||
|
|
||||||
|
return (<div>
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
<table className="table-auto border-collapse border-2 border-black w-fit">
|
||||||
|
<thead>
|
||||||
|
<tr className="*:p-1 *:border border-b-2 border-b-black">
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Grade</th>
|
||||||
|
<th>Class</th>
|
||||||
|
<th>Number</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>RFID</th>
|
||||||
|
<th>Admin?</th>
|
||||||
|
<th>Roster Only?</th>
|
||||||
|
<th>Operation</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
users.data?.map((user) => (
|
||||||
|
<tr className="*:p-1 *:border" key={user.username}>
|
||||||
|
<td>{user.username}</td>
|
||||||
|
<td>{user.grade}</td>
|
||||||
|
<td>{user.class}</td>
|
||||||
|
<td>{user.number}</td>
|
||||||
|
<td>{user.name}</td>
|
||||||
|
<td>{user.rfid}</td>
|
||||||
|
<td className="text-sky-600 underline text-center hover:cursor-pointer"
|
||||||
|
onClick={() => toggleAdmin.mutateAsync(user.username).then(() => users.refetch())}
|
||||||
|
>{user.isAdmin ? "V" : "X"}</td>
|
||||||
|
<td className="text-sky-600 underline text-center hover:cursor-pointer"
|
||||||
|
onClick={() => toggleRosterOnly.mutateAsync(user.username).then(() => users.refetch())}
|
||||||
|
>{user.rosterOnly ? "V" : "X"}</td>
|
||||||
|
<td className="text-center">
|
||||||
|
<button className="p-1 px-2 bg-yellow-600 text-white rounded-lg" onClick={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
resetPassword.mutateAsync(user.username)
|
||||||
|
.then(() => alert(`Password reset for ${user.username} to password.`))
|
||||||
|
.catch((err) => alert(err))
|
||||||
|
}} type="button">Reset Password</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<p className="my-2">For editing rows, please use backend SQL.</p>
|
||||||
|
<p className="my-2">Roster Only users must also be Admin.</p>
|
||||||
|
<hr />
|
||||||
|
<h2 className="text-xl mt-5 mb-2 font-bold">Create new user</h2>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="mb-5">
|
||||||
|
<label className="block text-md font-medium text-gray-700 mt-2">Username</label>
|
||||||
|
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-70" {...register("username")} />
|
||||||
|
{errors.username && <span className="text-red-500">{errors.username.message}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="mb-5">
|
||||||
|
<label className="block text-md font-medium text-gray-700 mt-2">Grade</label>
|
||||||
|
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-70" {...register("grade")} />
|
||||||
|
{errors.grade && <span className="text-red-500">{errors.grade.message}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="mb-5">
|
||||||
|
<label className="block text-md font-medium text-gray-700 mt-2">Class</label>
|
||||||
|
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-70" {...register("class")} />
|
||||||
|
{errors.class && <span className="text-red-500">{errors.class.message}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="mb-5">
|
||||||
|
<label className="block text-md font-medium text-gray-700 mt-2">Number</label>
|
||||||
|
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-70" {...register("number")} />
|
||||||
|
{errors.number && <span className="text-red-500">{errors.number.message}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="mb-5">
|
||||||
|
<label className="block text-md font-medium text-gray-700 mt-2">Name</label>
|
||||||
|
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-70" {...register("name")} />
|
||||||
|
{errors.name && <span className="text-red-500">{errors.name.message}</span>}
|
||||||
|
</div>
|
||||||
|
<button className="bg-emerald-600 px-3 py-2 rounded text-white focus:ring focus:ring-emerald-200 focus:ring-opacity-70 disabled:bg-emerald-400 mb-5" disabled={addUser.isLoading}>
|
||||||
|
Add User
|
||||||
|
</button>
|
||||||
|
<p>Default password is <code className="bg-gray-600 text-white p-1 rounded">password</code>.</p>
|
||||||
|
</form>
|
||||||
|
{addUser.isError && <p className="text-red-500">{addUser.error.message}</p>}
|
||||||
|
</div>)
|
||||||
|
}
|
26
src/pages/api/rfid.ts
Normal file
26
src/pages/api/rfid.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { setLastRfid } from '~/utils/rfid'
|
||||||
|
|
||||||
|
type ResponseData = {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<ResponseData>
|
||||||
|
) {
|
||||||
|
if ("uid" in req.body) {
|
||||||
|
try {
|
||||||
|
z.string().regex(/^[0-9a-f]{8}$/i).parse(req.body.uid)
|
||||||
|
} catch (e) {
|
||||||
|
res.status(200).json({ message: 'Invalid UID!' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLastRfid(req.body.uid)
|
||||||
|
res.status(200).json({ message: 'Received!' })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.status(200).json({ message: 'No UID!' })
|
||||||
|
}
|
||||||
|
}
|
53
src/pages/dash/admin/index.tsx
Normal file
53
src/pages/dash/admin/index.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import Head from "next/head";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import DashboardHeader from "~/components/DashboardHeader";
|
||||||
|
import Config from "~/components/admin/Config";
|
||||||
|
import Periods from "~/components/admin/Periods";
|
||||||
|
import Users from "~/components/admin/Users";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
|
export default function Dash() {
|
||||||
|
const { push } = useRouter();
|
||||||
|
const isLoggedIn = api.admin.isLoggedIn.useQuery();
|
||||||
|
|
||||||
|
const [currentPage, setCurrentPage] = useState("config");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoggedIn.failureCount > 0) {
|
||||||
|
push("/");
|
||||||
|
}
|
||||||
|
}, [isLoggedIn.failureCount])
|
||||||
|
|
||||||
|
if (isLoggedIn.isLoading) return <></>
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<Head>
|
||||||
|
<title>Savage Tracking</title>
|
||||||
|
<meta name="description" content="Time tracking app for FRC build season personnel management." />
|
||||||
|
<link rel="icon" href="/favicon.png" />
|
||||||
|
</Head>
|
||||||
|
<main className="">
|
||||||
|
<DashboardHeader url="/dash/admin" />
|
||||||
|
<div className="p-5 flex flex-col md:flex-row gap-5">
|
||||||
|
<nav className="flex flex-grow h-fit md:flex-col w-auto md:max-w-48 border border-emerald-700 rounded-lg">
|
||||||
|
<button className={[
|
||||||
|
"p-1 px-2 text-lg font-bold border-r md:border-b rounded-l-lg md:rounded-none md:rounded-t-lg transition-colors",
|
||||||
|
currentPage === "config" ? "bg-emerald-600 border-none text-white" : "bg-white text-emerald-700"
|
||||||
|
].join(" ")} onClick={() => setCurrentPage("config")}>Configuration</button>
|
||||||
|
<button className={[
|
||||||
|
"p-1 px-2 text-lg font-bold border-r md:border-b transition-colors",
|
||||||
|
currentPage === "periods" ? "bg-emerald-600 border-none text-white" : "bg-white text-emerald-700"
|
||||||
|
].join(" ")} onClick={() => setCurrentPage("periods")}>All Periods</button>
|
||||||
|
<button className={[
|
||||||
|
"p-1 px-2 text-lg font-bold rounded-r-lg md:rounded-none md:rounded-b-lg transition-colors",
|
||||||
|
currentPage === "users" ? "bg-emerald-600 border-none text-white" : "bg-white text-emerald-700"
|
||||||
|
].join(" ")} onClick={() => setCurrentPage("users")}>Users</button>
|
||||||
|
</nav>
|
||||||
|
{ currentPage === "config" && <Config /> }
|
||||||
|
{ currentPage === "users" && <Users /> }
|
||||||
|
{ currentPage === "periods" && <Periods /> }
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>)
|
||||||
|
}
|
79
src/pages/dash/admin/rfid.tsx
Normal file
79
src/pages/dash/admin/rfid.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import Head from "next/head";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import DashboardHeader from "~/components/DashboardHeader";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
|
export default function Dash() {
|
||||||
|
const { push } = useRouter();
|
||||||
|
const isLoggedIn = api.admin.isLoggedIn.useQuery();
|
||||||
|
const users = api.admin.getAllUsers.useQuery();
|
||||||
|
const lastRfid = api.admin.getLastRfid.useQuery();
|
||||||
|
const setUserRfid = api.admin.setUserRfid.useMutation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoggedIn.failureCount > 0) {
|
||||||
|
push("/");
|
||||||
|
}
|
||||||
|
}, [isLoggedIn.failureCount])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastRfid.isSuccess) {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
lastRfid.refetch();
|
||||||
|
}, 2000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (isLoggedIn.isLoading) return <></>
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<Head>
|
||||||
|
<title>Savage Tracking</title>
|
||||||
|
<meta name="description" content="Time tracking app for FRC build season personnel management." />
|
||||||
|
<link rel="icon" href="/favicon.png" />
|
||||||
|
</Head>
|
||||||
|
<main className="">
|
||||||
|
<DashboardHeader url="/dash/admin/rfid" />
|
||||||
|
<div className="m-5">
|
||||||
|
<table className="table-auto border-collapse border-2 border-black w-fit">
|
||||||
|
<thead>
|
||||||
|
<tr className="*:p-1 *:border border-b-2 border-b-black">
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Grade</th>
|
||||||
|
<th>Class</th>
|
||||||
|
<th>Number</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>RFID</th>
|
||||||
|
<th>Operation</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
users.data?.map((user) => (
|
||||||
|
<tr className="*:p-1 *:border" key={user.username}>
|
||||||
|
<td>{user.username}</td>
|
||||||
|
<td>{user.grade}</td>
|
||||||
|
<td>{user.class}</td>
|
||||||
|
<td>{user.number}</td>
|
||||||
|
<td>{user.name}</td>
|
||||||
|
<td>{user.rfid}</td>
|
||||||
|
<td className="text-emerald-600 underline text-center hover:cursor-pointer" onClick={() => {
|
||||||
|
setUserRfid.mutateAsync({
|
||||||
|
username: user.username,
|
||||||
|
rfid: lastRfid.data || ""
|
||||||
|
}).then(() => users.refetch())
|
||||||
|
}}>
|
||||||
|
Select
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h2 className="my-5 text-2xl">Last RFID: <code className="rounded bg-slate-700 text-white p-1 ml-1">{lastRfid.data}</code></h2>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>)
|
||||||
|
}
|
30
src/pages/dash/admin/roster.tsx
Normal file
30
src/pages/dash/admin/roster.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import Head from "next/head";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import DashboardHeader from "~/components/DashboardHeader";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
|
export default function Dash() {
|
||||||
|
const { push } = useRouter();
|
||||||
|
const isLoggedIn = api.admin.isLoggedIn.useQuery();
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoggedIn.failureCount > 0) {
|
||||||
|
push("/");
|
||||||
|
}
|
||||||
|
}, [isLoggedIn.failureCount])
|
||||||
|
|
||||||
|
if (isLoggedIn.isLoading) return <></>
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<Head>
|
||||||
|
<title>Savage Tracking</title>
|
||||||
|
<meta name="description" content="Time tracking app for FRC build season personnel management." />
|
||||||
|
<link rel="icon" href="/favicon.png" />
|
||||||
|
</Head>
|
||||||
|
<main className="">
|
||||||
|
<DashboardHeader url="/dash/admin/roster" />
|
||||||
|
</main>
|
||||||
|
</>)
|
||||||
|
}
|
63
src/pages/dash/chgPassword.tsx
Normal file
63
src/pages/dash/chgPassword.tsx
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Head from "next/head";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import DashboardHeader from "~/components/DashboardHeader";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { ChgPasswordSchema } from "~/utils/types";
|
||||||
|
|
||||||
|
export default function Dash() {
|
||||||
|
const { push } = useRouter();
|
||||||
|
const isLoggedIn = api.admin.isLoggedIn.useQuery();
|
||||||
|
const chgPassword = api.admin.chgPassword.useMutation();
|
||||||
|
const logout = api.admin.logout.useMutation();
|
||||||
|
type ChgPasswordSchemaType = z.infer<typeof ChgPasswordSchema>;
|
||||||
|
const { register, handleSubmit, formState: { errors } } = useForm<ChgPasswordSchemaType>({ resolver: zodResolver(ChgPasswordSchema) });
|
||||||
|
const onSubmit: SubmitHandler<ChgPasswordSchemaType> = async (data) => chgPassword.mutateAsync(data).catch((err) => console.log(err));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoggedIn.failureCount > 1) {
|
||||||
|
push("/");
|
||||||
|
}
|
||||||
|
}, [isLoggedIn.failureCount])
|
||||||
|
|
||||||
|
if (isLoggedIn.isLoading) return <></>
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (chgPassword.isSuccess) {
|
||||||
|
logout.mutateAsync().then(() => push("/"));
|
||||||
|
}
|
||||||
|
}, [chgPassword.isSuccess])
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<Head>
|
||||||
|
<title>Savage Tracking</title>
|
||||||
|
<meta name="description" content="Time tracking app for FRC build season personnel management." />
|
||||||
|
<link rel="icon" href="/favicon.png" />
|
||||||
|
</Head>
|
||||||
|
<main className="">
|
||||||
|
<DashboardHeader url="/dash/chgPassword" />
|
||||||
|
<div className="max-w-[40rem] p-5">
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="mb-5">
|
||||||
|
<label className="block text-md font-medium text-gray-700 mt-2">Old Password</label>
|
||||||
|
<input type="password" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-sky-300 focus:ring focus:ring-sky-200 focus:ring-opacity-70" {...register("oldPassword")} />
|
||||||
|
{errors.oldPassword && <span className="text-red-500">{errors.oldPassword.message}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="mb-7">
|
||||||
|
<label className="block text-md font-medium text-gray-700 mt-2">New Password</label>
|
||||||
|
<input type="password" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-sky-300 focus:ring focus:ring-sky-200 focus:ring-opacity-70" {...register("newPassword")} />
|
||||||
|
{errors.newPassword && <span>{errors.newPassword.message}</span>}
|
||||||
|
</div>
|
||||||
|
{chgPassword.isError && <div className="text-red-500 mb-5">{chgPassword.error.message}</div>}
|
||||||
|
{chgPassword.isSuccess && <div className="text-green-500 mb-5">Success! Please sign in again...</div>}
|
||||||
|
<button className="bg-sky-600 px-3 py-2 rounded text-white focus:ring focus:ring-sky-200 focus:ring-opacity-70 disabled:bg-sky-400 mb-5" disabled={chgPassword.isLoading}>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>)
|
||||||
|
}
|
|
@ -13,13 +13,12 @@ export default function Home() {
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
type LoginSchemaType = z.infer<typeof LoginSchema>;
|
type LoginSchemaType = z.infer<typeof LoginSchema>;
|
||||||
const { register, handleSubmit, formState: { errors } } = useForm<LoginSchemaType>({ resolver: zodResolver(LoginSchema) });
|
const { register, handleSubmit, formState: { errors } } = useForm<LoginSchemaType>({ resolver: zodResolver(LoginSchema) });
|
||||||
const onSubmit: SubmitHandler<LoginSchemaType> = async (data) =>
|
const onSubmit: SubmitHandler<LoginSchemaType> = async (data) => login.mutateAsync(data).catch((err) => console.log(err));
|
||||||
login.mutateAsync(data).catch((err) => console.log(err))
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (login.isSuccess) {
|
if (login.isSuccess) {
|
||||||
push("/dash")
|
push("/dash")
|
||||||
}
|
}
|
||||||
}, [login.isSuccess])
|
}, [login.isSuccess]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,38 +1,40 @@
|
||||||
import { compare, compareSync } from "bcrypt";
|
import { compareSync, hashSync } from "bcrypt";
|
||||||
import { z } from "zod";
|
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import { createTRPCRouter, loggedInProcedure, publicProcedure } from "~/server/api/trpc";
|
import { adminProcedure, createTRPCRouter, loggedInProcedure, publicProcedure } from "~/server/api/trpc";
|
||||||
import { LoginSchema, PublicUserType } from "~/utils/types";
|
import { AddPeriods, AddTimePeriodSchema, AddUserSchema, ChgPasswordSchema, LoginSchema, PublicUserType } from "~/utils/types";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Period } from "@prisma/client";
|
||||||
|
import { lastGotRfid } from "~/utils/rfid";
|
||||||
|
|
||||||
export const adminRouter = createTRPCRouter({
|
export const adminRouter = createTRPCRouter({
|
||||||
isLoggedIn: loggedInProcedure.query(() => true),
|
isLoggedIn: loggedInProcedure.query(() => true),
|
||||||
login: publicProcedure
|
login: publicProcedure
|
||||||
.input(LoginSchema)
|
.input(LoginSchema)
|
||||||
.mutation(async ({ input, ctx }) => await ctx.db.user.findUnique({
|
.mutation(async ({ input, ctx }) => await ctx.db.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
username: input.username,
|
username: input.username,
|
||||||
}
|
}
|
||||||
}).then((user) => {
|
}).then((user) => {
|
||||||
const result = compareSync(input.password, user?.password || "");
|
const result = compareSync(input.password, user?.password || "");
|
||||||
if (result) {
|
if (result) {
|
||||||
const session = PublicUserType.parse(user)!;
|
const session = PublicUserType.parse(user)!;
|
||||||
const token = jwt.sign(session, process.env.JWT_SECRET || "", { expiresIn: "1d" });
|
const token = jwt.sign(session, process.env.JWT_SECRET || "", { expiresIn: "1d" });
|
||||||
ctx.res.setHeader("Set-Cookie", `token=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${60 * 60 * 24};`);
|
ctx.res.setHeader("Set-Cookie", `token=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${60 * 60 * 24};`);
|
||||||
return {
|
return {
|
||||||
status: "success",
|
status: "success",
|
||||||
message: "Login successful.",
|
message: "Login successful.",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Please check your username and password.");
|
throw new Error("Please check your username and password.");
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
message: "Please check your username and password.",
|
message: "Please check your username and password.",
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
),
|
),
|
||||||
logout: loggedInProcedure
|
logout: loggedInProcedure
|
||||||
.mutation(async ({ ctx }) => {
|
.mutation(async ({ ctx }) => {
|
||||||
|
@ -46,4 +48,240 @@ export const adminRouter = createTRPCRouter({
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
return ctx.session;
|
return ctx.session;
|
||||||
}),
|
}),
|
||||||
|
chgPassword: loggedInProcedure
|
||||||
|
.input(ChgPasswordSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await ctx.db.user.findUnique({
|
||||||
|
where: {
|
||||||
|
username: ctx.session?.username,
|
||||||
|
}
|
||||||
|
}).then(async (user) => {
|
||||||
|
const result = compareSync(input.oldPassword, user?.password || "");
|
||||||
|
if (result) {
|
||||||
|
await ctx.db.user.update({
|
||||||
|
where: {
|
||||||
|
username: ctx.session?.username,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
password: hashSync(input.newPassword, 10),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
message: "Password changed successfully.",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Please check your old password.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Please check your old password.",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
getTimePeriods: loggedInProcedure
|
||||||
|
.query(async ({ ctx }) => {
|
||||||
|
return await ctx.db.timePeriod.findMany({
|
||||||
|
orderBy: {
|
||||||
|
id: "asc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
addTimePeriod: adminProcedure
|
||||||
|
.input(AddTimePeriodSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
return await ctx.db.timePeriod.create({
|
||||||
|
data: {
|
||||||
|
id: input.id,
|
||||||
|
name: input.name,
|
||||||
|
start: input.startTime,
|
||||||
|
end: input.endTime,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
deleteTimePeriod: adminProcedure
|
||||||
|
.input(z.number().int())
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
return await ctx.db.timePeriod.delete({
|
||||||
|
where: {
|
||||||
|
id: input,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
getAllUsers: adminProcedure
|
||||||
|
.query(async ({ ctx }) => {
|
||||||
|
return await ctx.db.user.findMany({
|
||||||
|
orderBy: {
|
||||||
|
username: "asc",
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
username: true,
|
||||||
|
name: true,
|
||||||
|
grade: true,
|
||||||
|
class: true,
|
||||||
|
number: true,
|
||||||
|
isAdmin: true,
|
||||||
|
rosterOnly: true,
|
||||||
|
rfid: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
resetPassword: adminProcedure
|
||||||
|
.input(z.string())
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
return await ctx.db.user.update({
|
||||||
|
where: {
|
||||||
|
username: input,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
password: hashSync("password", 10),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
toggleUserIsAdmin: adminProcedure
|
||||||
|
.input(z.string())
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
return await ctx.db.user.update({
|
||||||
|
where: {
|
||||||
|
username: input,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
isAdmin: await ctx.db.user.findUnique({
|
||||||
|
where: {
|
||||||
|
username: input,
|
||||||
|
},
|
||||||
|
}).then((user) => {
|
||||||
|
return !user?.isAdmin;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
toggleUserRosterOnly: adminProcedure
|
||||||
|
.input(z.string())
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
return await ctx.db.user.update({
|
||||||
|
where: {
|
||||||
|
username: input,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
rosterOnly: await ctx.db.user.findUnique({
|
||||||
|
where: {
|
||||||
|
username: input,
|
||||||
|
},
|
||||||
|
}).then((user) => {
|
||||||
|
return !user?.rosterOnly;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
addUser: adminProcedure
|
||||||
|
.input(AddUserSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
return await ctx.db.user.create({
|
||||||
|
data: {
|
||||||
|
username: input.username,
|
||||||
|
name: input.name,
|
||||||
|
grade: input.grade,
|
||||||
|
class: input.class,
|
||||||
|
number: input.number,
|
||||||
|
password: hashSync("password", 10),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
getPeriods: loggedInProcedure
|
||||||
|
.query(async ({ ctx }) => {
|
||||||
|
return await ctx.db.period.findMany({
|
||||||
|
orderBy: [{
|
||||||
|
date: "asc",
|
||||||
|
}, {
|
||||||
|
timePeriodId: "asc",
|
||||||
|
}],
|
||||||
|
}).then((periods) => {
|
||||||
|
const groupedPeriods: { [key: string]: Period[] } = {};
|
||||||
|
periods.forEach((period) => {
|
||||||
|
const utcDate = period.date.toLocaleDateString();
|
||||||
|
if (groupedPeriods[utcDate] === undefined) {
|
||||||
|
groupedPeriods[utcDate] = [];
|
||||||
|
}
|
||||||
|
groupedPeriods[utcDate]?.push(period);
|
||||||
|
});
|
||||||
|
return groupedPeriods;
|
||||||
|
}).catch((err) => {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "There was an error getting the periods.",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
addPeriods: adminProcedure
|
||||||
|
.input(AddPeriods)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await ctx.db.timePeriod.findMany().then(
|
||||||
|
async (timePeriods) => {
|
||||||
|
timePeriods.forEach(async (timePeriod) => {
|
||||||
|
await ctx.db.period.create({
|
||||||
|
data: {
|
||||||
|
date: new Date(input.date),
|
||||||
|
timePeriodId: timePeriod.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
message: "Periods added successfully.",
|
||||||
|
};
|
||||||
|
}).catch((err) => {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "There was an error adding the periods.",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
enablePeriod: adminProcedure
|
||||||
|
.input(z.object({
|
||||||
|
date: z.string(),
|
||||||
|
timePeriodId: z.number().int(),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
return await ctx.db.period.create({
|
||||||
|
data: {
|
||||||
|
date: new Date(input.date),
|
||||||
|
timePeriodId: input.timePeriodId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
disablePeriod: adminProcedure
|
||||||
|
.input(z.number().int())
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
return await ctx.db.period.delete({
|
||||||
|
where: {
|
||||||
|
id: input,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
getLastRfid: adminProcedure
|
||||||
|
.query(() => {
|
||||||
|
return lastGotRfid;
|
||||||
|
}),
|
||||||
|
setUserRfid: adminProcedure
|
||||||
|
.input(z.object({
|
||||||
|
username: z.string(),
|
||||||
|
rfid: z.string(),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
return await ctx.db.user.update({
|
||||||
|
where: {
|
||||||
|
username: input.username,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
rfid: input.rfid,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
3
src/utils/config.ts
Normal file
3
src/utils/config.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const config = {
|
||||||
|
latestSignup: "2024-01-01T00:00:00.000Z",
|
||||||
|
}
|
4
src/utils/rfid.ts
Normal file
4
src/utils/rfid.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export let lastGotRfid = "";
|
||||||
|
export const setLastRfid = (uid: string) => {
|
||||||
|
lastGotRfid = uid;
|
||||||
|
};
|
|
@ -6,9 +6,34 @@ export const PublicUserType = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
isAdmin: z.boolean(),
|
isAdmin: z.boolean(),
|
||||||
|
rosterOnly: z.boolean().default(false),
|
||||||
}).or(z.undefined());
|
}).or(z.undefined());
|
||||||
|
|
||||||
export const LoginSchema = z.object({
|
export const LoginSchema = z.object({
|
||||||
username: z.string().min(1, { message: "Username cannot be empty." }),
|
username: z.string().min(1, { message: "Username cannot be empty." }),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const ChgPasswordSchema = z.object({
|
||||||
|
oldPassword: z.string().min(1, { message: "Old password cannot be empty." }),
|
||||||
|
newPassword: z.string().min(8, { message: "New password must be at least 8 characters long." }),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AddTimePeriodSchema = z.object({
|
||||||
|
id: z.coerce.number().int({ message: "ID must be an integer." }).min(1, { message: "ID must be greater than 0." }).max(999, { message: "ID must be less than 1000." }),
|
||||||
|
name: z.string().min(1, { message: "Name cannot be empty." }),
|
||||||
|
startTime: z.string().regex(/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/, { message: "Start time must be in the format HH:MM."}),
|
||||||
|
endTime: z.string().regex(/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/, { message: "End time must be in the format HH:MM."}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AddUserSchema = z.object({
|
||||||
|
username: z.string().min(1, { message: "Username cannot be empty." }),
|
||||||
|
name: z.string().min(1, { message: "Name cannot be empty." }),
|
||||||
|
grade: z.coerce.number().int().nullable(),
|
||||||
|
class: z.string().nullable(),
|
||||||
|
number: z.coerce.number().int().nullable(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AddPeriods = z.object({
|
||||||
|
date: z.string().regex(/^20[2-3][0-9]\/(0?[1-9]|1[0-2]){1}\/(0?[1-9]|1[0-9]|2[0-9]|3[0-1]){1}$/, { message: "Periods must be in the format YYYY/MM/DD."})
|
||||||
|
})
|
Loading…
Reference in a new issue