diff --git a/.env.example b/.env.example index 4cf7769..87d1436 100644 --- a/.env.example +++ b/.env.example @@ -13,4 +13,5 @@ # https://www.prisma.io/docs/reference/database-reference/connection-urls#env DATABASE_URL="file:./db.sqlite" JWT_SECRET="well, now the secret is spoiled, isn't it?" -TZ="Asia/Taipei" \ No newline at end of file +TZ="Asia/Taipei" +RFID_PKEY="secret" \ No newline at end of file diff --git a/src/components/admin/Periods.tsx b/src/components/admin/Periods.tsx index 496ba5e..641d25f 100644 --- a/src/components/admin/Periods.tsx +++ b/src/components/admin/Periods.tsx @@ -54,7 +54,10 @@ export default function Periods() { if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) { periodCnt++; return ( 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 ) } else { @@ -73,6 +76,7 @@ export default function Periods() { } +

WARNING: Disabling periods will cause records to be deleted as well!


Add Date

diff --git a/src/pages/api/rfid.ts b/src/pages/api/rfid.ts index 2aa9cb8..6a49f9b 100644 --- a/src/pages/api/rfid.ts +++ b/src/pages/api/rfid.ts @@ -1,6 +1,8 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { z } from 'zod' +import { appRouter } from '~/server/api/root' import { setLastRfid } from '~/utils/rfid' +import { db } from '~/server/db' type ResponseData = { message: string @@ -12,15 +14,16 @@ export default function handler( ) { if ("uid" in req.body) { 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) { - res.status(200).json({ message: 'Invalid UID!' }) - return + res.status(200).json({ message: 'Invalid UID!' }); + return; } - setLastRfid(req.body.uid) - res.status(200).json({ message: 'Received!' }) + setLastRfid(req.body.uid); + const caller = appRouter.createCaller({ session: undefined, db, req, res }); + res.status(200).json({ message: 'Received!' }); } else { - res.status(200).json({ message: 'No UID!' }) + res.status(200).json({ message: 'No UID!' }); } } diff --git a/src/pages/dash/admin/rfid.tsx b/src/pages/dash/admin/rfid.tsx index 042d641..8dacfde 100644 --- a/src/pages/dash/admin/rfid.tsx +++ b/src/pages/dash/admin/rfid.tsx @@ -10,6 +10,8 @@ export default function Dash() { const users = api.admin.getAllUsers.useQuery(); const lastRfid = api.admin.getLastRfid.useQuery(); const setUserRfid = api.admin.setUserRfid.useMutation(); + const rfidAttendance = api.admin.rfidAttendance.useQuery(); + const toggleRfidAttendance = api.admin.toggleRfidAttendance.useMutation(); useEffect(() => { if (isLoggedIn.failureCount > 0) { @@ -37,6 +39,15 @@ export default function Dash() {
+ RFID Attendance: {rfidAttendance.data?.rfidAttendance ? "Enabled" : "Disabled"} + @@ -60,6 +71,7 @@ export default function Dash() {
{user.name} {user.rfid} { + if (lastRfid.data === "") return; setUserRfid.mutateAsync({ username: user.username, rfid: lastRfid.data || "" diff --git a/src/pages/dash/index.tsx b/src/pages/dash/index.tsx index 2e643c3..80db379 100644 --- a/src/pages/dash/index.tsx +++ b/src/pages/dash/index.tsx @@ -7,12 +7,17 @@ import { api } from "~/utils/api"; export default function Dash() { const { push } = useRouter(); 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(() => { if (isLoggedIn.failureCount > 0) { push("/"); - } + } }, [isLoggedIn.failureCount]) if (isLoggedIn.isLoading) return <> @@ -25,6 +30,67 @@ export default function Dash() {
+
+
= 30 ? "bg-emerald-500" : "bg-red-300" + ].join(" ")}> +
Hours
+
+
{attendTime.data}
+
+
+
Click cells below to toggle
+ + + + + { + timePeriods.data?.map((timePeriods) => { + return () + }) + } + + + + { + Object.keys(periods.data || {}).map((date) => { + periodCnt = 0; + return ( + + + { + 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 + } else { + return + } + } else { + return + } + }) + } + + ) + }) + } + +
Date{timePeriods.name} ({timePeriods.start} ~ {timePeriods.end})
{date} toggleAttendance.mutateAsync({ + periodId: thisPeriodId || -1, + attendance: false + }).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}> + Will Attend + toggleAttendance.mutateAsync({ + periodId: thisPeriodId || -1, + attendance: true + }).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}>N/A
+
) } \ No newline at end of file diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 434dd54..db689d6 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -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 { adminRouter } from "./routers/admin"; @@ -8,7 +8,7 @@ import { adminRouter } from "./routers/admin"; * All routers added in /api/routers should be manually added here. */ export const appRouter = createTRPCRouter({ - // post: postRouter, + timeSel: timeSelRouter, admin: adminRouter, }); diff --git a/src/server/api/routers/admin.ts b/src/server/api/routers/admin.ts index e9d0af7..6ac0d8b 100644 --- a/src/server/api/routers/admin.ts +++ b/src/server/api/routers/admin.ts @@ -6,7 +6,7 @@ import { AddPeriods, AddTimePeriodSchema, AddUserSchema, ChgPasswordSchema, Logi import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { Period } from "@prisma/client"; -import { lastGotRfid } from "~/utils/rfid"; +import { lastGotRfid, rfidAttendance, setRfidAttendance } from "~/utils/rfid"; export const adminRouter = createTRPCRouter({ isLoggedIn: loggedInProcedure.query(() => true), @@ -220,7 +220,18 @@ export const adminRouter = createTRPCRouter({ addPeriods: adminProcedure .input(AddPeriods) .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) => { timePeriods.forEach(async (timePeriod) => { await ctx.db.period.create({ @@ -249,6 +260,16 @@ export const adminRouter = createTRPCRouter({ timePeriodId: z.number().int(), })) .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({ data: { date: new Date(input.date), @@ -257,11 +278,15 @@ export const adminRouter = createTRPCRouter({ }) }), disablePeriod: adminProcedure - .input(z.number().int()) + .input(z.object({ + date: z.string(), + timePeriodId: z.number().int(), + })) .mutation(async ({ input, ctx }) => { - return await ctx.db.period.delete({ + return await ctx.db.period.deleteMany({ 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.", + }; + }), }); diff --git a/src/server/api/routers/post.ts b/src/server/api/routers/post.ts deleted file mode 100644 index 68367a3..0000000 --- a/src/server/api/routers/post.ts +++ /dev/null @@ -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" }, - }); - }), -}); diff --git a/src/server/api/routers/time-sel.ts b/src/server/api/routers/time-sel.ts new file mode 100644 index 0000000..97e8896 --- /dev/null +++ b/src/server/api/routers/time-sel.ts @@ -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; + }), +}); diff --git a/src/utils/rfid.ts b/src/utils/rfid.ts index 5cf6ebb..53637ad 100644 --- a/src/utils/rfid.ts +++ b/src/utils/rfid.ts @@ -1,4 +1,8 @@ export let lastGotRfid = ""; export const setLastRfid = (uid: string) => { lastGotRfid = uid; +}; +export let rfidAttendance = true; +export const setRfidAttendance = (value: boolean) => { + rfidAttendance = value; }; \ No newline at end of file