added user select period

This commit is contained in:
Aaron Lee 2023-12-24 23:26:56 +08:00
parent 54cdb7e83f
commit b5f7f9ef3c
10 changed files with 236 additions and 49 deletions

View file

@ -14,3 +14,4 @@
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" TZ="Asia/Taipei"
RFID_PKEY="secret"

View file

@ -54,7 +54,10 @@ export default function Periods() {
if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) { if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) {
periodCnt++; periodCnt++;
return (<td key={timePeriod.id * periodCnt} className="bg-emerald-600 text-white hover:cursor-pointer" 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())}> onClick={() => disablePeriod.mutateAsync({
date: date,
timePeriodId: periods.data![date]![thisPeriodCnt]!.timePeriodId
}).then(() => periods.refetch())}>
Disable Disable
</td>) </td>)
} else { } else {
@ -73,6 +76,7 @@ export default function Periods() {
} }
</tbody> </tbody>
</table> </table>
<p className="text-red-500 font-bold mb-5">WARNING: Disabling periods will cause records to be deleted as well!</p>
<hr /> <hr />
<h2 className="text-xl font-bold mt-5">Add Date</h2> <h2 className="text-xl font-bold mt-5">Add Date</h2>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>

View file

@ -1,6 +1,8 @@
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from 'next'
import { z } from 'zod' import { z } from 'zod'
import { appRouter } from '~/server/api/root'
import { setLastRfid } from '~/utils/rfid' import { setLastRfid } from '~/utils/rfid'
import { db } from '~/server/db'
type ResponseData = { type ResponseData = {
message: string message: string
@ -12,15 +14,16 @@ export default function handler(
) { ) {
if ("uid" in req.body) { if ("uid" in req.body) {
try { try {
z.string().regex(/^[0-9a-f]{8}$/i).parse(req.body.uid) z.string().regex(/^[0-9a-f]{8}$/i).parse(req.body.uid);
} catch (e) { } catch (e) {
res.status(200).json({ message: 'Invalid UID!' }) res.status(200).json({ message: 'Invalid UID!' });
return return;
} }
setLastRfid(req.body.uid) setLastRfid(req.body.uid);
res.status(200).json({ message: 'Received!' }) const caller = appRouter.createCaller({ session: undefined, db, req, res });
res.status(200).json({ message: 'Received!' });
} }
else { else {
res.status(200).json({ message: 'No UID!' }) res.status(200).json({ message: 'No UID!' });
} }
} }

View file

@ -10,6 +10,8 @@ export default function Dash() {
const users = api.admin.getAllUsers.useQuery(); const users = api.admin.getAllUsers.useQuery();
const lastRfid = api.admin.getLastRfid.useQuery(); const lastRfid = api.admin.getLastRfid.useQuery();
const setUserRfid = api.admin.setUserRfid.useMutation(); const setUserRfid = api.admin.setUserRfid.useMutation();
const rfidAttendance = api.admin.rfidAttendance.useQuery();
const toggleRfidAttendance = api.admin.toggleRfidAttendance.useMutation();
useEffect(() => { useEffect(() => {
if (isLoggedIn.failureCount > 0) { if (isLoggedIn.failureCount > 0) {
@ -37,6 +39,15 @@ export default function Dash() {
<main className=""> <main className="">
<DashboardHeader url="/dash/admin/rfid" /> <DashboardHeader url="/dash/admin/rfid" />
<div className="m-5"> <div className="m-5">
RFID Attendance: {rfidAttendance.data?.rfidAttendance ? "Enabled" : "Disabled"}
<button className={[
"p-1 px-2 text-lg font-bold rounded mb-2 ml-2 text-white transition-colors",
rfidAttendance.data?.rfidAttendance ? "bg-yellow-600 hover:bg-yellow-700" : "bg-green-600 hover:bg-green-700"
].join(" ")} onClick={() => toggleRfidAttendance.mutateAsync()
.then(() => rfidAttendance.refetch())
.catch((err) => console.log(err))}>
{rfidAttendance.data?.rfidAttendance ? "Disable" : "Enable"}
</button>
<table className="table-auto border-collapse border-2 border-black w-fit"> <table className="table-auto border-collapse border-2 border-black w-fit">
<thead> <thead>
<tr className="*:p-1 *:border border-b-2 border-b-black"> <tr className="*:p-1 *:border border-b-2 border-b-black">
@ -60,6 +71,7 @@ export default function Dash() {
<td>{user.name}</td> <td>{user.name}</td>
<td>{user.rfid}</td> <td>{user.rfid}</td>
<td className="text-emerald-600 underline text-center hover:cursor-pointer" onClick={() => { <td className="text-emerald-600 underline text-center hover:cursor-pointer" onClick={() => {
if (lastRfid.data === "") return;
setUserRfid.mutateAsync({ setUserRfid.mutateAsync({
username: user.username, username: user.username,
rfid: lastRfid.data || "" rfid: lastRfid.data || ""

View file

@ -7,7 +7,12 @@ import { api } from "~/utils/api";
export default function Dash() { export default function Dash() {
const { push } = useRouter(); const { push } = useRouter();
const isLoggedIn = api.admin.isLoggedIn.useQuery(); const isLoggedIn = api.admin.isLoggedIn.useQuery();
const periods = api.admin.getPeriods.useQuery();
const timePeriods = api.admin.getTimePeriods.useQuery();
const mySelectedPeriods = api.timeSel.getMySelectedPeriods.useQuery();
const toggleAttendance = api.timeSel.toggleAttendance.useMutation();
const attendTime = api.timeSel.attendTime.useQuery();
let periodCnt = 0;
useEffect(() => { useEffect(() => {
if (isLoggedIn.failureCount > 0) { if (isLoggedIn.failureCount > 0) {
@ -25,6 +30,67 @@ export default function Dash() {
</Head> </Head>
<main className=""> <main className="">
<DashboardHeader url="/dash" /> <DashboardHeader url="/dash" />
<div className="p-5">
<div className={[
"border rounded-xl h-32 w-40 p-2 flex items-center justify-center flex-col mb-5",
(attendTime.data || 0) >= 30 ? "bg-emerald-500" : "bg-red-300"
].join(" ")}>
<div className="text-center text-3xl font-bold">Hours</div>
<div className="flex-grow flex items-center">
<div className="text-center text-6xl font-bold">{attendTime.data}</div>
</div>
</div>
<div className="text-2xl font-bold mb-2">Click cells below to toggle</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 thisPeriodId = periods.data![date]![periodCnt]?.id!;
if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) {
periodCnt++;
if (mySelectedPeriods.data?.findIndex((period) => period == thisPeriodId) != -1) {
return <td key={timePeriod.id * periodCnt} className="bg-emerald-200 hover:cursor-pointer"
onClick={() => toggleAttendance.mutateAsync({
periodId: thisPeriodId || -1,
attendance: false
}).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}>
Will Attend
</td>
} else {
return <td key={timePeriod.id * periodCnt} className="bg-sky-100 hover:cursor-pointer"
onClick={() => toggleAttendance.mutateAsync({
periodId: thisPeriodId || -1,
attendance: true
}).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}></td>
}
} else {
return <td key={timePeriod.id * periodCnt} className="bg-gray-400">N/A</td>
}
})
}
</tr>
)
})
}
</tbody>
</table>
</div>
</main> </main>
</>) </>)
} }

View file

@ -1,4 +1,4 @@
import { postRouter } from "~/server/api/routers/post"; import { timeSelRouter } from "~/server/api/routers/time-sel";
import { createTRPCRouter } from "~/server/api/trpc"; import { createTRPCRouter } from "~/server/api/trpc";
import { adminRouter } from "./routers/admin"; import { adminRouter } from "./routers/admin";
@ -8,7 +8,7 @@ import { adminRouter } from "./routers/admin";
* All routers added in /api/routers should be manually added here. * All routers added in /api/routers should be manually added here.
*/ */
export const appRouter = createTRPCRouter({ export const appRouter = createTRPCRouter({
// post: postRouter, timeSel: timeSelRouter,
admin: adminRouter, admin: adminRouter,
}); });

View file

@ -6,7 +6,7 @@ import { AddPeriods, AddTimePeriodSchema, AddUserSchema, ChgPasswordSchema, Logi
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { Period } from "@prisma/client"; import { Period } from "@prisma/client";
import { lastGotRfid } from "~/utils/rfid"; import { lastGotRfid, rfidAttendance, setRfidAttendance } from "~/utils/rfid";
export const adminRouter = createTRPCRouter({ export const adminRouter = createTRPCRouter({
isLoggedIn: loggedInProcedure.query(() => true), isLoggedIn: loggedInProcedure.query(() => true),
@ -220,7 +220,18 @@ export const adminRouter = createTRPCRouter({
addPeriods: adminProcedure addPeriods: adminProcedure
.input(AddPeriods) .input(AddPeriods)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await ctx.db.timePeriod.findMany().then( const period = await ctx.db.period.findFirst({
where: {
date: new Date(input.date),
},
});
if (period !== null) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Periods with date already exist.",
})
}
return await ctx.db.timePeriod.findMany().then(
async (timePeriods) => { async (timePeriods) => {
timePeriods.forEach(async (timePeriod) => { timePeriods.forEach(async (timePeriod) => {
await ctx.db.period.create({ await ctx.db.period.create({
@ -249,6 +260,16 @@ export const adminRouter = createTRPCRouter({
timePeriodId: z.number().int(), timePeriodId: z.number().int(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
// if data exists, do nothing
const period = await ctx.db.period.findFirst({
where: {
date: new Date(input.date),
timePeriodId: input.timePeriodId,
},
});
if (period !== null) {
return period;
}
return await ctx.db.period.create({ return await ctx.db.period.create({
data: { data: {
date: new Date(input.date), date: new Date(input.date),
@ -257,11 +278,15 @@ export const adminRouter = createTRPCRouter({
}) })
}), }),
disablePeriod: adminProcedure disablePeriod: adminProcedure
.input(z.number().int()) .input(z.object({
date: z.string(),
timePeriodId: z.number().int(),
}))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
return await ctx.db.period.delete({ return await ctx.db.period.deleteMany({
where: { where: {
id: input, date: new Date(input.date),
timePeriodId: input.timePeriodId,
}, },
}) })
}), }),
@ -284,4 +309,19 @@ export const adminRouter = createTRPCRouter({
}, },
}) })
}), }),
rfidAttendance: adminProcedure
.query(() => {
return {
status: "success",
rfidAttendance: rfidAttendance,
}
}),
toggleRfidAttendance: adminProcedure
.mutation(() => {
setRfidAttendance(!rfidAttendance);
return {
status: "success",
message: "RFID attendance toggled.",
};
}),
}); });

View file

@ -1,32 +0,0 @@
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
export const postRouter = createTRPCRouter({
hello: publicProcedure
.input(z.object({ text: z.string() }))
.query(({ input }) => {
return {
greeting: `Hello ${input.text}`,
};
}),
create: publicProcedure
.input(z.object({ name: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
// simulate a slow db call
await new Promise((resolve) => setTimeout(resolve, 1000));
return ctx.db.post.create({
data: {
name: input.name,
},
});
}),
getLatest: publicProcedure.query(({ ctx }) => {
return ctx.db.post.findFirst({
orderBy: { createdAt: "desc" },
});
}),
});

View file

@ -0,0 +1,89 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, loggedInProcedure, publicProcedure } from "~/server/api/trpc";
export const timeSelRouter = createTRPCRouter({
getMySelectedPeriods: loggedInProcedure
.query(async ({ ctx }) => {
const user = await ctx.db.user.findUnique({
where: { username: ctx.session?.username },
select: {
periods: {
select: {
id: true,
}
}
}
});
if (!user) throw new TRPCError({ code: "UNAUTHORIZED", message: "User not found" });
return user.periods.map(period => period.id);
}),
toggleAttendance: loggedInProcedure
.input(z.object({
periodId: z.number().int(),
attendance: z.boolean(),
}))
.mutation(async ({ ctx, input }) => {
try {
if (input.attendance) {
await ctx.db.period.update({
where: { id: input.periodId },
data: {
users: {
connect: {
username: ctx.session?.username,
}
}
}
});
} else {
await ctx.db.period.update({
where: { id: input.periodId },
data: {
users: {
disconnect: {
username: ctx.session?.username,
}
}
}
});
}
} catch (e) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "An error occurred." });
}
return true;
}),
attendTime: loggedInProcedure
.query(async ({ ctx }) => {
const user = await ctx.db.user.findUnique({
where: { username: ctx.session?.username },
select: {
periods: {
select: {
timePeriod: {
select: {
start: true,
end: true,
}
}
}
}
}
});
if (!user) throw new TRPCError({ code: "UNAUTHORIZED", message: "User not found" });
let total = 0;
user.periods.forEach(period => {
const start = new Date();
const end = new Date();
const [startHour, startMinute] = period.timePeriod.start.split(":");
const [endHour, endMinute] = period.timePeriod.end.split(":");
start.setHours(parseInt(startHour!));
start.setMinutes(parseInt(startMinute!));
end.setHours(parseInt(endHour!));
end.setMinutes(parseInt(endMinute!));
total += (end.getTime() - start.getTime()) / 1000 / 60 / 60; // convert to hours
})
return total;
}),
});

View file

@ -2,3 +2,7 @@ export let lastGotRfid = "";
export const setLastRfid = (uid: string) => { export const setLastRfid = (uid: string) => {
lastGotRfid = uid; lastGotRfid = uid;
}; };
export let rfidAttendance = true;
export const setRfidAttendance = (value: boolean) => {
rfidAttendance = value;
};