mirror of
https://github.com/aaronleetw/savage-tracking.git
synced 2024-11-14 11:01:39 -08:00
wrote multiple parts; attendance todo
This commit is contained in:
parent
b5f7f9ef3c
commit
7b895a20c1
17 changed files with 591 additions and 61 deletions
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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[]
|
||||
|
||||
|
@ -58,3 +61,15 @@ model Period {
|
|||
|
||||
@@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])
|
||||
}
|
|
@ -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
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/dash/chgPassword">
|
||||
|
@ -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
|
||||
</button>
|
||||
</Link>
|
||||
{
|
||||
|
@ -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
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/dash/admin/rfid">
|
||||
|
@ -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
|
||||
</button>
|
||||
</Link>
|
||||
</>)
|
||||
|
|
|
@ -21,9 +21,9 @@ export default function Config() {
|
|||
<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">
|
||||
<table className="table-auto border-collapse w-fit border-2 border-black">
|
||||
<thead>
|
||||
<tr className="*:p-1 *:border">
|
||||
<tr className="*:p-1 *:border border-b-2 border-b-black">
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Start</th>
|
||||
|
@ -50,16 +50,16 @@ export default function Config() {
|
|||
}
|
||||
<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")} />
|
||||
<input type="number" className="mt-1 block w-16 rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-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")} />
|
||||
<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" 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")} />
|
||||
<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" 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")} />
|
||||
<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" 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>
|
||||
|
|
|
@ -30,6 +30,7 @@ export default function Users() {
|
|||
<th>Class</th>
|
||||
<th>Number</th>
|
||||
<th>Name</th>
|
||||
<th>Display Name</th>
|
||||
<th>RFID</th>
|
||||
<th>Admin?</th>
|
||||
<th>Roster Only?</th>
|
||||
|
@ -45,6 +46,7 @@ export default function Users() {
|
|||
<td>{user.class}</td>
|
||||
<td>{user.number}</td>
|
||||
<td>{user.name}</td>
|
||||
<td>{user.dname}</td>
|
||||
<td>{user.rfid}</td>
|
||||
<td className="text-sky-600 underline text-center hover:cursor-pointer"
|
||||
onClick={() => toggleAdmin.mutateAsync(user.username).then(() => users.refetch())}
|
||||
|
@ -97,6 +99,11 @@ export default function Users() {
|
|||
<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>
|
||||
<div className="mb-5">
|
||||
<label className="block text-md font-medium text-gray-700 mt-2">Display 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("dname")} />
|
||||
{errors.dname && <span className="text-red-500">{errors.dname.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>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
/**
|
||||
|
|
|
@ -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 <main className={noto.className}><Component {...pageProps} /></main>;
|
||||
return <main className={`${kai.variable} ${noto.className}`}><Component {...pageProps} /></main>;
|
||||
};
|
||||
|
||||
export default api.withTRPC(MyApp);
|
||||
|
|
|
@ -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<ResponseData>
|
||||
) {
|
||||
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' });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
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 isLoggedIn = api.admin.isLoggedIn.useQuery();
|
||||
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) {
|
||||
|
@ -25,6 +35,86 @@ export default function Dash() {
|
|||
</Head>
|
||||
<main className="">
|
||||
<DashboardHeader url="/dash/admin/roster" />
|
||||
<div className="m-5">
|
||||
<div className="flex gap-5 items-end">
|
||||
<label className="block max-w-72 mb-5">
|
||||
<span className="text-gray-700">Date</span>
|
||||
<select className="block w-full mt-1 rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-50"
|
||||
onChange={(evt) => {
|
||||
setDate(new Date(evt.target.value));
|
||||
roster.refetch().then(() => {
|
||||
setGenDate(new Date());
|
||||
});
|
||||
}}>
|
||||
<option value="">Select a date</option>
|
||||
{
|
||||
Object.keys(periods.data || {}).map((date) => {
|
||||
return (<option key={date}>{date}</option>)
|
||||
})
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<button className="h-fit 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={roster.isLoading}
|
||||
onClick={() => {
|
||||
toPDF();
|
||||
}}>
|
||||
Download PDF
|
||||
</button>
|
||||
{
|
||||
(roster.isLoading) && <div role="status">
|
||||
<svg aria-hidden="true" className="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-emerald-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" />
|
||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" />
|
||||
</svg>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<hr />
|
||||
<div className="pdf my-5 w-fit p-5" ref={pdfRef}>
|
||||
<h1 className="text-3xl font-extrabold font-kai mb-2">機器人研究社 Build Season 入校名單</h1>
|
||||
<div className="flex mb-2 justify-between font-extrabold font-kai items-end">
|
||||
<h2 className="text-2xl font-kai">日期: {date.toLocaleDateString()}</h2>
|
||||
<h3 className="m-0">列印時間: {genDate.toLocaleString()}</h3>
|
||||
</div>
|
||||
<table className="table-auto border-collapse border-2 border-black w-fit mt-3 font-kai text-lg">
|
||||
<thead>
|
||||
<tr className="*:p-1 *:border border-b-2 border-b-black">
|
||||
<th>年級</th>
|
||||
<th>班級</th>
|
||||
<th>座號</th>
|
||||
<th>姓名</th>
|
||||
{
|
||||
timePeriods.data?.map((timePeriods) => {
|
||||
return (<th key={timePeriods.id}>{timePeriods.name} ({timePeriods.start} ~ {timePeriods.end})</th>)
|
||||
})
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
roster.data?.map((user) => {
|
||||
return (<tr key={user.id} className="*:p-1 *:border">
|
||||
<td>{user.grade}</td>
|
||||
<td>{user.class}</td>
|
||||
<td>{user.number}</td>
|
||||
<td>{user.name}</td>
|
||||
{
|
||||
timePeriods.data?.map((timePeriod) => {
|
||||
if (user.periods.find((period) => period.timePeriod.id === timePeriod.id)) {
|
||||
return (<td key={timePeriod.id}>✔</td>)
|
||||
} else {
|
||||
return (<td key={timePeriod.id}></td>)
|
||||
}
|
||||
})
|
||||
}
|
||||
</tr>)
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>)
|
||||
}
|
|
@ -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() {
|
|||
<main className="">
|
||||
<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 className="flex gap-2">
|
||||
<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-2xl font-bold">已選取時數</div>
|
||||
<div className="flex-grow flex items-center">
|
||||
<div className="text-center text-6xl font-bold">{attendTime.data}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={[
|
||||
"border rounded-xl h-32 w-40 p-2 flex items-center justify-center flex-col mb-5",
|
||||
(actualAttendTime.data || 0) >= 30 ? "bg-emerald-500" : "bg-red-300"
|
||||
].join(" ")}>
|
||||
<div className="text-center text-xl font-bold">實際出席時數</div>
|
||||
<div className="flex-grow flex items-center">
|
||||
<div className="text-center text-6xl font-bold">{actualAttendTime.data?.toPrecision(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold mb-2">Click cells below to toggle</div>
|
||||
<div className="text-2xl font-bold mb-2">請點選下方切換狀態 (上方綠燈 > 100)</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">
|
||||
|
@ -56,33 +79,73 @@ export default function Dash() {
|
|||
{
|
||||
Object.keys(periods.data || {}).map((date) => {
|
||||
periodCnt = 0;
|
||||
entered = false;
|
||||
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>
|
||||
// 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 <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-sky-100 hover:cursor-pointer"
|
||||
onClick={() => toggleAttendance.mutateAsync({
|
||||
periodId: thisPeriodId || -1,
|
||||
attendance: true
|
||||
}).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}></td>
|
||||
return <td key={timePeriod.id * periodCnt} className="bg-gray-400">N/A</td>
|
||||
}
|
||||
} else {
|
||||
return <td key={timePeriod.id * periodCnt} className="bg-gray-400">N/A</td>
|
||||
}
|
||||
})
|
||||
})
|
||||
) : (
|
||||
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 <td key={timePeriod.id * periodCnt} className="bg-green-700 text-white"></td>
|
||||
}
|
||||
if (entered && periodCnt === timePeriods.data!.length) {
|
||||
return <td key={timePeriod.id * periodCnt} className="bg-yellow-700 text-white">{data}</td>
|
||||
}
|
||||
if (data === "") {
|
||||
return <td key={timePeriod.id * periodCnt} className="bg-gray-500 text-white">Absent</td>
|
||||
}
|
||||
return <td key={timePeriod.id * periodCnt} className="bg-green-700 text-white">{data}</td>
|
||||
})
|
||||
)
|
||||
}
|
||||
</tr>
|
||||
)
|
||||
|
|
BIN
src/pages/kaiu.ttf
Normal file
BIN
src/pages/kaiu.ttf
Normal file
Binary file not shown.
|
@ -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",
|
||||
},
|
||||
]
|
||||
})
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -7,6 +7,7 @@ export default {
|
|||
extend: {
|
||||
fontFamily: {
|
||||
sans: ["Lato", "var(--font-sans)", ...fontFamily.sans],
|
||||
kai: ["var(--font-kai)", "Lato", ...fontFamily.serif],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
145
yarn.lock
145
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"
|
||||
|
|
Loading…
Reference in a new issue