diff --git a/package.json b/package.json index 08bc747..d4c4fc9 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.49.2", + "react-to-pdf": "^1.0.1", "superjson": "^2.2.1", "zod": "^3.22.4" }, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 384f4c3..9351e95 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,6 +15,7 @@ model User { grade Int? class String? name String + dname String username String @unique password String rfid String? @unique @@ -22,6 +23,7 @@ model User { rosterOnly Boolean @default(false) periods Period[] + attendance Attendance[] number Int? createdAt DateTime @default(now()) @@ -48,8 +50,9 @@ model TimePeriod { model Period { id Int @id @default(autoincrement()) date DateTime - timePeriod TimePeriod @relation(fields: [timePeriodId], references: [id]) + timePeriod TimePeriod @relation(fields: [timePeriodId], references: [id], onDelete: Cascade) timePeriodId Int + collecting Boolean @default(true) users User[] @@ -57,4 +60,16 @@ model Period { updatedAt DateTime @updatedAt @@index([date]) +} + +model Attendance { + id Int @id @default(autoincrement()) + datetime DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([datetime]) } \ No newline at end of file diff --git a/src/components/DashboardHeader.tsx b/src/components/DashboardHeader.tsx index ed52d73..d335fb6 100644 --- a/src/components/DashboardHeader.tsx +++ b/src/components/DashboardHeader.tsx @@ -40,7 +40,7 @@ export default function DashboardHeader({ url }: { url: string }) { "text-sm md:text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-sky-200 focus:ring-opacity-70", url == "/dash" ? "bg-sky-600 text-white" : "bg-sky-300 text-black" ].join(" ")}> - Time Selection + Attendance @@ -61,7 +61,7 @@ export default function DashboardHeader({ url }: { url: string }) { "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" ].join(" ")}> - View Roster + Roster { @@ -71,7 +71,7 @@ export default function DashboardHeader({ url }: { url: string }) { "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 + Settings @@ -79,7 +79,7 @@ export default function DashboardHeader({ url }: { url: string }) { "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 + RFID ) diff --git a/src/components/admin/Config.tsx b/src/components/admin/Config.tsx index bdd092f..8d7f475 100644 --- a/src/components/admin/Config.tsx +++ b/src/components/admin/Config.tsx @@ -21,9 +21,9 @@ export default function Config() {

Current time periods:

- +
- + @@ -50,16 +50,16 @@ export default function Config() { } + @@ -45,6 +46,7 @@ export default function Users() { +
ID Name Start
- + - + - + - + diff --git a/src/components/admin/Users.tsx b/src/components/admin/Users.tsx index 66e3e3e..64652bb 100644 --- a/src/components/admin/Users.tsx +++ b/src/components/admin/Users.tsx @@ -30,6 +30,7 @@ export default function Users() { Class Number NameDisplay Name RFID Admin? Roster Only?{user.class} {user.number} {user.name}{user.dname} {user.rfid} toggleAdmin.mutateAsync(user.username).then(() => users.refetch())} @@ -97,6 +99,11 @@ export default function Users() { {errors.name && {errors.name.message}} +
+ + + {errors.dname && {errors.dname.message}} +
diff --git a/src/env.js b/src/env.js index 1107bd0..0307a33 100644 --- a/src/env.js +++ b/src/env.js @@ -17,6 +17,9 @@ export const env = createEnv({ NODE_ENV: z .enum(["development", "test", "production"]) .default("development"), + JWT_SECRET: z.string().min(32).default("changeme"), + TZ: z.string().default("UTC"), + RFID_PKEY: z.string().default("changeme"), }, /** @@ -35,6 +38,9 @@ export const env = createEnv({ runtimeEnv: { DATABASE_URL: process.env.DATABASE_URL, NODE_ENV: process.env.NODE_ENV, + JWT_SECRET: process.env.JWT_SECRET, + TZ: process.env.TZ, + RFID_PKEY: process.env.RFID_PKEY, // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, }, /** diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 3c11075..fff7f4c 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,15 +1,17 @@ import { type AppType } from "next/app"; import { Noto_Sans } from "next/font/google" +import localFont from 'next/font/local' import { api } from "~/utils/api"; import "~/styles/globals.css"; const noto = Noto_Sans({ subsets: ["latin"] }) +const kai = localFont({ src: "./kaiu.ttf", variable: "--font-kai"}) const MyApp: AppType = ({ Component, pageProps }) => { - return
; + return
; }; export default api.withTRPC(MyApp); diff --git a/src/pages/api/rfid.ts b/src/pages/api/rfid.ts index 6a49f9b..5dde305 100644 --- a/src/pages/api/rfid.ts +++ b/src/pages/api/rfid.ts @@ -1,29 +1,45 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { z } from 'zod' import { appRouter } from '~/server/api/root' -import { setLastRfid } from '~/utils/rfid' +import { rfidAttendance, setLastRfid } from '~/utils/rfid' import { db } from '~/server/db' +import { env } from 'process' type ResponseData = { - message: string + status: string, + name?: string } -export default function handler( +export default async function handler( req: NextApiRequest, res: NextApiResponse ) { - if ("uid" in req.body) { + if (!req.body || !("key" in req.body) || !(req.body.key === process.env.RFID_PKEY)) { + res.status(403).json({ status: 'ERR_BADKEY' }); + return; + } + else 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!' }); + res.status(200).json({ status: 'ERR_BADUID' }); return; } setLastRfid(req.body.uid); - const caller = appRouter.createCaller({ session: undefined, db, req, res }); - res.status(200).json({ message: 'Received!' }); + if (rfidAttendance) { + const caller = appRouter.createCaller({ session: undefined, db, req, res }); + try { + const resp = await caller.admin.rfidAttendance({ rfid: req.body.uid, key: req.body.key }); + res.status(200).json(resp); + } catch (e) { + res.status(500).json({ status: "ERR_INTERNAL" }); + return; + } + } else { + res.status(200).json({ status: 'RFID_ATTENDANCE_NOT_ENABLED' }); + } } else { - res.status(200).json({ message: 'No UID!' }); + res.status(400).json({ status: 'ERR_NOUID' }); } } diff --git a/src/pages/dash/admin/rfid.tsx b/src/pages/dash/admin/rfid.tsx index 8dacfde..0e20301 100644 --- a/src/pages/dash/admin/rfid.tsx +++ b/src/pages/dash/admin/rfid.tsx @@ -10,8 +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(); + const rfidAttendance = api.admin.getRfidAttendanceForDash.useQuery(); + const toggleRfidAttendance = api.admin.toggleRfidAttendanceForDash.useMutation(); useEffect(() => { if (isLoggedIn.failureCount > 0) { diff --git a/src/pages/dash/admin/roster.tsx b/src/pages/dash/admin/roster.tsx index 20e7a93..b7e57ef 100644 --- a/src/pages/dash/admin/roster.tsx +++ b/src/pages/dash/admin/roster.tsx @@ -1,18 +1,28 @@ import Head from "next/head"; import { useRouter } from "next/navigation"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; +import { usePDF } from "react-to-pdf"; import DashboardHeader from "~/components/DashboardHeader"; import { api } from "~/utils/api"; export default function Dash() { const { push } = useRouter(); + const [date, setDate] = useState(new Date()); + const [genDate, setGenDate] = useState(new Date(0)); + + const { toPDF, targetRef: pdfRef } = usePDF({ filename: `入校名單_${date.toLocaleDateString().replace("/", "-")}.pdf` }); + const isLoggedIn = api.admin.isLoggedIn.useQuery(); - + const periods = api.admin.getPeriods.useQuery(); + const timePeriods = api.admin.getTimePeriods.useQuery(); + const roster = api.admin.getRoster.useQuery({ + date: date + }); useEffect(() => { if (isLoggedIn.failureCount > 0) { push("/"); - } + } }, [isLoggedIn.failureCount]) if (isLoggedIn.isLoading) return <> @@ -25,6 +35,86 @@ export default function Dash() {
+
+
+ + + { + (roster.isLoading) &&
+ + Loading... +
+ } +
+
+
+

機器人研究社 Build Season 入校名單

+
+

日期: {date.toLocaleDateString()}

+

列印時間: {genDate.toLocaleString()}

+
+ + + + + + + + { + timePeriods.data?.map((timePeriods) => { + return () + }) + } + + + + { + roster.data?.map((user) => { + return ( + + + + + { + timePeriods.data?.map((timePeriod) => { + if (user.periods.find((period) => period.timePeriod.id === timePeriod.id)) { + return () + } else { + return () + } + }) + } + ) + }) + } + +
年級班級座號姓名{timePeriods.name} ({timePeriods.start} ~ {timePeriods.end})
{user.grade}{user.class}{user.number}{user.name}
+
+
) } \ No newline at end of file diff --git a/src/pages/dash/index.tsx b/src/pages/dash/index.tsx index 80db379..b4003ff 100644 --- a/src/pages/dash/index.tsx +++ b/src/pages/dash/index.tsx @@ -10,9 +10,21 @@ export default function Dash() { const periods = api.admin.getPeriods.useQuery(); const timePeriods = api.admin.getTimePeriods.useQuery(); const mySelectedPeriods = api.timeSel.getMySelectedPeriods.useQuery(); + const myAttendance = api.timeSel.getMyAttendance.useQuery(); const toggleAttendance = api.timeSel.toggleAttendance.useMutation(); const attendTime = api.timeSel.attendTime.useQuery(); + const actualAttendTime = api.timeSel.actualAttendTime.useQuery(); + let periodCnt = 0; + let attCnt = 0; + let entered = false; + + const fuseDateAndTime = (date: string, time: Date) => { + const year = new Date(date).getFullYear(); + const month = new Date(date).getMonth(); + const day = new Date(date).getDate(); + return new Date(year, month, day, time.getHours(), time.getMinutes(), time.getSeconds(), time.getMilliseconds()); + } useEffect(() => { if (isLoggedIn.failureCount > 0) { @@ -31,16 +43,27 @@ export default function Dash() {
-
= 30 ? "bg-emerald-500" : "bg-red-300" - ].join(" ")}> -
Hours
-
-
{attendTime.data}
+
+
= 30 ? "bg-emerald-500" : "bg-red-300" + ].join(" ")}> +
已選取時數
+
+
{attendTime.data}
+
+
+
= 30 ? "bg-emerald-500" : "bg-red-300" + ].join(" ")}> +
實際出席時數
+
+
{actualAttendTime.data?.toPrecision(2)}
+
-
Click cells below to toggle
+
請點選下方切換狀態 (上方綠燈 > 100)
@@ -56,33 +79,73 @@ export default function Dash() { { Object.keys(periods.data || {}).map((date) => { periodCnt = 0; + entered = false; 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 + // TODO: fix this very ugly code + new Date(new Date(date).setDate(new Date(date).getDate() - 1)).setHours(23, 59, 59, 999) > new Date().getTime() ? ( + 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 + return } - } else { - return - } - }) + }) + ) : ( + timePeriods.data?.map((timePeriod) => { + periodCnt++; + + let data = ""; + const thisPeriodStart = new Date(date); + thisPeriodStart.setHours(parseInt(timePeriod.start.split(":")[0]!), parseInt(timePeriod.start.split(":")[1]!), 0, 0) + const thisPeriodEnd = new Date(date); + thisPeriodEnd.setHours(parseInt(timePeriod.end.split(":")[0]!), parseInt(timePeriod.end.split(":")[1]!), 0, 0) + + for (let i = attCnt; i < (myAttendance.data || []).length; i++) { + const thisAtt = myAttendance.data![i]; + if (thisAtt?.datetime! < thisPeriodStart) continue; + if (thisAtt?.datetime! > thisPeriodEnd) { + attCnt = i; + break; + } + if (!entered) { + data += `${data !== "" ? " / " : ""}${thisAtt?.datetime!.toLocaleTimeString()} ~ `; + } else { + data += `${thisAtt?.datetime!.toLocaleTimeString()}`; + } + entered = !entered; + } + + if (entered && data === "" && periodCnt !== timePeriods.data!.length) { + return + } + if (entered && periodCnt === timePeriods.data!.length) { + return + } + if (data === "") { + return + } + return + }) + ) } ) diff --git a/src/pages/kaiu.ttf b/src/pages/kaiu.ttf new file mode 100644 index 0000000..d8a755f Binary files /dev/null and b/src/pages/kaiu.ttf differ diff --git a/src/server/api/routers/admin.ts b/src/server/api/routers/admin.ts index 6ac0d8b..d32f777 100644 --- a/src/server/api/routers/admin.ts +++ b/src/server/api/routers/admin.ts @@ -121,6 +121,7 @@ export const adminRouter = createTRPCRouter({ select: { username: true, name: true, + dname: true, grade: true, class: true, number: true, @@ -185,6 +186,7 @@ export const adminRouter = createTRPCRouter({ data: { username: input.username, name: input.name, + dname: input.dname, grade: input.grade, class: input.class, number: input.number, @@ -203,11 +205,11 @@ export const adminRouter = createTRPCRouter({ }).then((periods) => { const groupedPeriods: { [key: string]: Period[] } = {}; periods.forEach((period) => { - const utcDate = period.date.toLocaleDateString(); - if (groupedPeriods[utcDate] === undefined) { - groupedPeriods[utcDate] = []; + const dateStr = period.date.toLocaleDateString(); + if (groupedPeriods[dateStr] === undefined) { + groupedPeriods[dateStr] = []; } - groupedPeriods[utcDate]?.push(period); + groupedPeriods[dateStr]?.push(period); }); return groupedPeriods; }).catch((err) => { @@ -309,14 +311,62 @@ export const adminRouter = createTRPCRouter({ }, }) }), - rfidAttendance: adminProcedure + getRfidAttendanceForDash: adminProcedure .query(() => { return { - status: "success", rfidAttendance: rfidAttendance, } }), - toggleRfidAttendance: adminProcedure + rfidAttendance: publicProcedure + .input(z.object({ rfid: z.string().regex(/^[0-9a-f]{8}$/i), key: z.string() })) + .query(async ({ input, ctx }) => { + if (input.key !== process.env.RFID_PKEY) { + return { + status: "ERR_BADKEY", + } + } + return await ctx.db.attendance.create({ + data: { + datetime: new Date(), + user: { + connect: { + rfid: input.rfid, + }, + }, + } + }).then(async () => { + const records = await ctx.db.attendance.findMany({ + where: { + datetime: { + gte: new Date(new Date().setHours(0, 0, 0, 0)), + lt: new Date(new Date().setHours(23, 59, 59, 999)), + }, + user: { + rfid: input.rfid, + }, + }, + select: { + user: { + select: { + dname: true, + }, + }, + }, + orderBy: { + datetime: "asc", + }, + }) + return { + status: records.length % 2 ? "WELCOME" : "GOODBYE", + name: records[records.length - 1]!.user.dname, + } + }).catch((err) => { + return { + status: "ERR_INTERNAL", + } + }) + }), + toggleRfidAttendanceForDash: adminProcedure .mutation(() => { setRfidAttendance(!rfidAttendance); return { @@ -324,4 +374,47 @@ export const adminRouter = createTRPCRouter({ message: "RFID attendance toggled.", }; }), + getRoster: adminProcedure + .input(z.object({ date: z.date()})) + .query(async ({ input, ctx }) => { + return await ctx.db.user.findMany({ + where: { + periods: { + some: { + date: input.date, + }, + } + }, + select: { + id: true, + name: true, + grade: true, + class: true, + number: true, + periods: { + where: { + date: input.date, + }, + select: { + timePeriod: { + select: { + id: true, + }, + }, + }, + }, + }, + orderBy: [ + { + grade: "asc", + }, + { + class: "asc", + }, + { + number: "asc", + }, + ] + }) + }), }); diff --git a/src/server/api/routers/time-sel.ts b/src/server/api/routers/time-sel.ts index 97e8896..f0616b9 100644 --- a/src/server/api/routers/time-sel.ts +++ b/src/server/api/routers/time-sel.ts @@ -26,6 +26,7 @@ export const timeSelRouter = createTRPCRouter({ })) .mutation(async ({ ctx, input }) => { try { + if (input.attendance) { await ctx.db.period.update({ where: { id: input.periodId }, @@ -86,4 +87,93 @@ export const timeSelRouter = createTRPCRouter({ }) return total; }), + getMyAttendance: loggedInProcedure + .query(async ({ ctx }) => { + return await ctx.db.attendance.findMany({ + where: { + user: { + username: ctx.session?.username, + } + }, + select: { + datetime: true, + }, + orderBy: { + datetime: "asc", + }, + }); + }), + actualAttendTime: loggedInProcedure + .query(async ({ ctx }) => { + const attendance = await ctx.db.attendance.findMany({ + where: { + user: { + username: ctx.session?.username, + } + }, + select: { + datetime: true, + }, + orderBy: { + datetime: "asc", + }, + }); + const dates = await ctx.db.period.findMany({ + select: { + date: true, + }, + orderBy: { + date: "asc", + } + }).then((periods) => { + const dates: string[] = []; + periods.forEach(period => { + const dateStr = period.date.toLocaleDateString(); + if (!dates.includes(dateStr)) dates.push(dateStr); + }) + return dates; + }) + const timePeriods = await ctx.db.timePeriod.findMany({ + select: { + start: true, + end: true, + }, + orderBy: [ + { start: "asc" }, + { end: "asc" }, + ] + }); + let attCnt = 0; + let calculator = 0.0; + dates.forEach((date) => { + let periodCnt = 0; + let entered = false; + timePeriods.forEach((timePeriod) => { + periodCnt++; + const thisPeriodStart = new Date(date); + thisPeriodStart.setHours(parseInt(timePeriod.start.split(":")[0]!), parseInt(timePeriod.start.split(":")[1]!), 0, 0) + const thisPeriodEnd = new Date(date); + thisPeriodEnd.setHours(parseInt(timePeriod.end.split(":")[0]!), parseInt(timePeriod.end.split(":")[1]!), 0, 0) + + for (let i = attCnt; i < attendance.length; i++) { + const thisAtt = attendance[i]!; + if (thisAtt.datetime < thisPeriodStart) continue; + if (thisAtt.datetime > thisPeriodEnd) { + attCnt = i; + break; + } + if (!entered) { + calculator -= thisAtt.datetime.getTime(); + } else { + calculator += thisAtt.datetime.getTime(); + } + entered = !entered; + } + if (entered && periodCnt === timePeriods.length) { + calculator += thisPeriodEnd.getTime(); + } + }) + }) + return calculator / 1000 / 60 / 60; + }), }); diff --git a/src/utils/types.ts b/src/utils/types.ts index 527fa68..7d4aee3 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -29,6 +29,7 @@ export const AddTimePeriodSchema = z.object({ 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." }), + dname: z.string().min(1, { message: "Display name cannot be empty." }).regex(/^[a-z0-9_\-]+$/i, { message: "Display name must only contain letters, numbers, underscores, and dashes." }), grade: z.coerce.number().int().nullable(), class: z.string().nullable(), number: z.coerce.number().int().nullable(), diff --git a/tailwind.config.ts b/tailwind.config.ts index 328fe7f..d34a398 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -7,6 +7,7 @@ export default { extend: { fontFamily: { sans: ["Lato", "var(--font-sans)", ...fontFamily.sans], + kai: ["var(--font-kai)", "Lato", ...fontFamily.serif], }, }, }, diff --git a/yarn.lock b/yarn.lock index ec3fea0..83028b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,13 @@ resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== +"@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d" + integrity sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ== + dependencies: + regenerator-runtime "^0.14.0" + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -346,6 +353,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng== +"@types/raf@^3.4.0": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.3.tgz#85f1d1d17569b28b8db45e16e996407a56b0ab04" + integrity sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw== + "@types/react-dom@^18.2.15": version "18.2.18" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd" @@ -547,6 +559,11 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + autoprefixer@^10.4.14: version "10.4.16" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.16.tgz#fad1411024d8670880bdece3970aa72e3572feb8" @@ -564,6 +581,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-arraybuffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" + integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== + bcrypt@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.1.tgz#0f732c6dcb4e12e5b70a25e326a72965879ba6e2" @@ -602,6 +624,11 @@ browserslist@^4.21.10: node-releases "^2.0.14" update-browserslist-db "^1.0.13" +btoa@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" + integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -629,6 +656,20 @@ caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.300015 resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz#b4e5c1fa786f733ab78fc70f592df6b3f23244ca" integrity sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw== +canvg@^3.0.6: + version "3.0.10" + resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.10.tgz#8e52a2d088b6ffa23ac78970b2a9eebfae0ef4b3" + integrity sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q== + dependencies: + "@babel/runtime" "^7.12.5" + "@types/raf" "^3.4.0" + core-js "^3.8.3" + raf "^3.4.1" + regenerator-runtime "^0.13.7" + rgbcolor "^1.0.1" + stackblur-canvas "^2.0.0" + svg-pathdata "^6.0.3" + chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -701,6 +742,11 @@ copy-anything@^3.0.2: dependencies: is-what "^4.1.8" +core-js@^3.6.0, core-js@^3.8.3: + version "3.35.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.35.0.tgz#58e651688484f83c34196ca13f099574ee53d6b4" + integrity sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg== + cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -710,6 +756,13 @@ cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +css-line-break@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" + integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== + dependencies: + utrie "^1.0.2" + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -766,6 +819,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dompurify@^2.2.0: + version "2.4.7" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.7.tgz#277adeb40a2c84be2d42a8bcd45f582bfa4d0cfc" + integrity sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ== + ecdsa-sig-formatter@1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" @@ -916,6 +974,11 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fflate@^0.4.8: + version "0.4.8" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" + integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -1095,6 +1158,14 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +html2canvas@^1.0.0-rc.5, html2canvas@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" + integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== + dependencies: + css-line-break "^2.1.0" + text-segmentation "^1.0.3" + https-proxy-agent@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" @@ -1233,6 +1304,21 @@ jsonwebtoken@^9.0.2: ms "^2.1.1" semver "^7.5.4" +jspdf@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-2.5.1.tgz#00c85250abf5447a05f3b32ab9935ab4a56592cc" + integrity sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA== + dependencies: + "@babel/runtime" "^7.14.0" + atob "^2.1.2" + btoa "^1.2.1" + fflate "^0.4.8" + optionalDependencies: + canvg "^3.0.6" + core-js "^3.6.0" + dompurify "^2.2.0" + html2canvas "^1.0.0-rc.5" + jwa@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" @@ -1570,6 +1656,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -1684,6 +1775,13 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +raf@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + react-dom@18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" @@ -1702,6 +1800,14 @@ react-ssr-prepass@^1.5.0: resolved "https://registry.yarnpkg.com/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz#bc4ca7fcb52365e6aea11cc254a3d1bdcbd030c5" integrity sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ== +react-to-pdf@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/react-to-pdf/-/react-to-pdf-1.0.1.tgz#a9a54ba9af9df4213078bf34eb2ffabb752b7263" + integrity sha512-ZsIkY6Z5gg3oBhMbWfl+tYwQ12vpPuuAzvCv+MnXchO8l08tElzRkBNAXxfbQNG/EDOHgE5EvWBlvE7ypt/y9A== + dependencies: + html2canvas "^1.4.1" + jspdf "^2.5.1" + react@18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -1732,6 +1838,16 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +regenerator-runtime@^0.13.7: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -1751,6 +1867,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rgbcolor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d" + integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw== + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -1821,6 +1942,11 @@ source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +stackblur-canvas@^2.0.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.6.0.tgz#7876bab4ea99bfc97b69ce662614d7a1afb2d71b" + integrity sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg== + streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" @@ -1893,6 +2019,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svg-pathdata@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac" + integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw== + tailwindcss@^3.3.5: version "3.4.0" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.0.tgz#045a9c474e6885ebd0436354e611a76af1c76839" @@ -1933,6 +2064,13 @@ tar@^6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" +text-segmentation@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943" + integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== + dependencies: + utrie "^1.0.2" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -2026,6 +2164,13 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +utrie@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645" + integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== + dependencies: + base64-arraybuffer "^1.0.2" + watchpack@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
{date} toggleAttendance.mutateAsync({ - periodId: thisPeriodId || -1, - attendance: false - }).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}> - Will Attend - 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())}> toggleAttendance.mutateAsync({ - periodId: thisPeriodId || -1, - attendance: true - }).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}>N/AN/A{data}Absent{data}