mirror of
https://github.com/aaronleetw/savage-tracking.git
synced 2024-11-14 19:11:39 -08:00
added user select period
This commit is contained in:
parent
54cdb7e83f
commit
b5f7f9ef3c
10 changed files with 236 additions and 49 deletions
|
@ -14,3 +14,4 @@
|
||||||
DATABASE_URL="file:./db.sqlite"
|
DATABASE_URL="file:./db.sqlite"
|
||||||
JWT_SECRET="well, now the secret is spoiled, isn't it?"
|
JWT_SECRET="well, now the secret is spoiled, isn't it?"
|
||||||
TZ="Asia/Taipei"
|
TZ="Asia/Taipei"
|
||||||
|
RFID_PKEY="secret"
|
|
@ -54,7 +54,10 @@ export default function Periods() {
|
||||||
if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) {
|
if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) {
|
||||||
periodCnt++;
|
periodCnt++;
|
||||||
return (<td key={timePeriod.id * periodCnt} className="bg-emerald-600 text-white hover:cursor-pointer"
|
return (<td key={timePeriod.id * periodCnt} className="bg-emerald-600 text-white hover:cursor-pointer"
|
||||||
onClick={() => disablePeriod.mutateAsync(periods.data![date]![thisPeriodCnt]!.id!).then(() => periods.refetch())}>
|
onClick={() => disablePeriod.mutateAsync({
|
||||||
|
date: date,
|
||||||
|
timePeriodId: periods.data![date]![thisPeriodCnt]!.timePeriodId
|
||||||
|
}).then(() => periods.refetch())}>
|
||||||
Disable
|
Disable
|
||||||
</td>)
|
</td>)
|
||||||
} else {
|
} else {
|
||||||
|
@ -73,6 +76,7 @@ export default function Periods() {
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<p className="text-red-500 font-bold mb-5">WARNING: Disabling periods will cause records to be deleted as well!</p>
|
||||||
<hr />
|
<hr />
|
||||||
<h2 className="text-xl font-bold mt-5">Add Date</h2>
|
<h2 className="text-xl font-bold mt-5">Add Date</h2>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { appRouter } from '~/server/api/root'
|
||||||
import { setLastRfid } from '~/utils/rfid'
|
import { setLastRfid } from '~/utils/rfid'
|
||||||
|
import { db } from '~/server/db'
|
||||||
|
|
||||||
type ResponseData = {
|
type ResponseData = {
|
||||||
message: string
|
message: string
|
||||||
|
@ -12,15 +14,16 @@ export default function handler(
|
||||||
) {
|
) {
|
||||||
if ("uid" in req.body) {
|
if ("uid" in req.body) {
|
||||||
try {
|
try {
|
||||||
z.string().regex(/^[0-9a-f]{8}$/i).parse(req.body.uid)
|
z.string().regex(/^[0-9a-f]{8}$/i).parse(req.body.uid);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(200).json({ message: 'Invalid UID!' })
|
res.status(200).json({ message: 'Invalid UID!' });
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
setLastRfid(req.body.uid)
|
setLastRfid(req.body.uid);
|
||||||
res.status(200).json({ message: 'Received!' })
|
const caller = appRouter.createCaller({ session: undefined, db, req, res });
|
||||||
|
res.status(200).json({ message: 'Received!' });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
res.status(200).json({ message: 'No UID!' })
|
res.status(200).json({ message: 'No UID!' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ export default function Dash() {
|
||||||
const users = api.admin.getAllUsers.useQuery();
|
const users = api.admin.getAllUsers.useQuery();
|
||||||
const lastRfid = api.admin.getLastRfid.useQuery();
|
const lastRfid = api.admin.getLastRfid.useQuery();
|
||||||
const setUserRfid = api.admin.setUserRfid.useMutation();
|
const setUserRfid = api.admin.setUserRfid.useMutation();
|
||||||
|
const rfidAttendance = api.admin.rfidAttendance.useQuery();
|
||||||
|
const toggleRfidAttendance = api.admin.toggleRfidAttendance.useMutation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoggedIn.failureCount > 0) {
|
if (isLoggedIn.failureCount > 0) {
|
||||||
|
@ -37,6 +39,15 @@ export default function Dash() {
|
||||||
<main className="">
|
<main className="">
|
||||||
<DashboardHeader url="/dash/admin/rfid" />
|
<DashboardHeader url="/dash/admin/rfid" />
|
||||||
<div className="m-5">
|
<div className="m-5">
|
||||||
|
RFID Attendance: {rfidAttendance.data?.rfidAttendance ? "Enabled" : "Disabled"}
|
||||||
|
<button className={[
|
||||||
|
"p-1 px-2 text-lg font-bold rounded mb-2 ml-2 text-white transition-colors",
|
||||||
|
rfidAttendance.data?.rfidAttendance ? "bg-yellow-600 hover:bg-yellow-700" : "bg-green-600 hover:bg-green-700"
|
||||||
|
].join(" ")} onClick={() => toggleRfidAttendance.mutateAsync()
|
||||||
|
.then(() => rfidAttendance.refetch())
|
||||||
|
.catch((err) => console.log(err))}>
|
||||||
|
{rfidAttendance.data?.rfidAttendance ? "Disable" : "Enable"}
|
||||||
|
</button>
|
||||||
<table className="table-auto border-collapse border-2 border-black w-fit">
|
<table className="table-auto border-collapse border-2 border-black w-fit">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="*:p-1 *:border border-b-2 border-b-black">
|
<tr className="*:p-1 *:border border-b-2 border-b-black">
|
||||||
|
@ -60,6 +71,7 @@ export default function Dash() {
|
||||||
<td>{user.name}</td>
|
<td>{user.name}</td>
|
||||||
<td>{user.rfid}</td>
|
<td>{user.rfid}</td>
|
||||||
<td className="text-emerald-600 underline text-center hover:cursor-pointer" onClick={() => {
|
<td className="text-emerald-600 underline text-center hover:cursor-pointer" onClick={() => {
|
||||||
|
if (lastRfid.data === "") return;
|
||||||
setUserRfid.mutateAsync({
|
setUserRfid.mutateAsync({
|
||||||
username: user.username,
|
username: user.username,
|
||||||
rfid: lastRfid.data || ""
|
rfid: lastRfid.data || ""
|
||||||
|
|
|
@ -7,7 +7,12 @@ import { api } from "~/utils/api";
|
||||||
export default function Dash() {
|
export default function Dash() {
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
const isLoggedIn = api.admin.isLoggedIn.useQuery();
|
const isLoggedIn = api.admin.isLoggedIn.useQuery();
|
||||||
|
const periods = api.admin.getPeriods.useQuery();
|
||||||
|
const timePeriods = api.admin.getTimePeriods.useQuery();
|
||||||
|
const mySelectedPeriods = api.timeSel.getMySelectedPeriods.useQuery();
|
||||||
|
const toggleAttendance = api.timeSel.toggleAttendance.useMutation();
|
||||||
|
const attendTime = api.timeSel.attendTime.useQuery();
|
||||||
|
let periodCnt = 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoggedIn.failureCount > 0) {
|
if (isLoggedIn.failureCount > 0) {
|
||||||
|
@ -25,6 +30,67 @@ export default function Dash() {
|
||||||
</Head>
|
</Head>
|
||||||
<main className="">
|
<main className="">
|
||||||
<DashboardHeader url="/dash" />
|
<DashboardHeader url="/dash" />
|
||||||
|
<div className="p-5">
|
||||||
|
<div className={[
|
||||||
|
"border rounded-xl h-32 w-40 p-2 flex items-center justify-center flex-col mb-5",
|
||||||
|
(attendTime.data || 0) >= 30 ? "bg-emerald-500" : "bg-red-300"
|
||||||
|
].join(" ")}>
|
||||||
|
<div className="text-center text-3xl font-bold">Hours</div>
|
||||||
|
<div className="flex-grow flex items-center">
|
||||||
|
<div className="text-center text-6xl font-bold">{attendTime.data}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold mb-2">Click cells below to toggle</div>
|
||||||
|
<table className="table-auto border-collapse border-2 border-black w-fit mb-5">
|
||||||
|
<thead>
|
||||||
|
<tr className="*:p-1 *:border border-b-2 border-b-black">
|
||||||
|
<th>Date</th>
|
||||||
|
{
|
||||||
|
timePeriods.data?.map((timePeriods) => {
|
||||||
|
return (<th key={timePeriods.id}>{timePeriods.name} ({timePeriods.start} ~ {timePeriods.end})</th>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
Object.keys(periods.data || {}).map((date) => {
|
||||||
|
periodCnt = 0;
|
||||||
|
return (
|
||||||
|
<tr className="*:p-1 *:border" key={date}>
|
||||||
|
<td>{date}</td>
|
||||||
|
{
|
||||||
|
timePeriods.data?.map((timePeriod) => {
|
||||||
|
const thisPeriodId = periods.data![date]![periodCnt]?.id!;
|
||||||
|
if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) {
|
||||||
|
periodCnt++;
|
||||||
|
if (mySelectedPeriods.data?.findIndex((period) => period == thisPeriodId) != -1) {
|
||||||
|
return <td key={timePeriod.id * periodCnt} className="bg-emerald-200 hover:cursor-pointer"
|
||||||
|
onClick={() => toggleAttendance.mutateAsync({
|
||||||
|
periodId: thisPeriodId || -1,
|
||||||
|
attendance: false
|
||||||
|
}).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}>
|
||||||
|
Will Attend
|
||||||
|
</td>
|
||||||
|
} else {
|
||||||
|
return <td key={timePeriod.id * periodCnt} className="bg-sky-100 hover:cursor-pointer"
|
||||||
|
onClick={() => toggleAttendance.mutateAsync({
|
||||||
|
periodId: thisPeriodId || -1,
|
||||||
|
attendance: true
|
||||||
|
}).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}></td>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return <td key={timePeriod.id * periodCnt} className="bg-gray-400">N/A</td>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { postRouter } from "~/server/api/routers/post";
|
import { timeSelRouter } from "~/server/api/routers/time-sel";
|
||||||
import { createTRPCRouter } from "~/server/api/trpc";
|
import { createTRPCRouter } from "~/server/api/trpc";
|
||||||
import { adminRouter } from "./routers/admin";
|
import { adminRouter } from "./routers/admin";
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import { adminRouter } from "./routers/admin";
|
||||||
* All routers added in /api/routers should be manually added here.
|
* All routers added in /api/routers should be manually added here.
|
||||||
*/
|
*/
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
// post: postRouter,
|
timeSel: timeSelRouter,
|
||||||
admin: adminRouter,
|
admin: adminRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { AddPeriods, AddTimePeriodSchema, AddUserSchema, ChgPasswordSchema, Logi
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Period } from "@prisma/client";
|
import { Period } from "@prisma/client";
|
||||||
import { lastGotRfid } from "~/utils/rfid";
|
import { lastGotRfid, rfidAttendance, setRfidAttendance } from "~/utils/rfid";
|
||||||
|
|
||||||
export const adminRouter = createTRPCRouter({
|
export const adminRouter = createTRPCRouter({
|
||||||
isLoggedIn: loggedInProcedure.query(() => true),
|
isLoggedIn: loggedInProcedure.query(() => true),
|
||||||
|
@ -220,7 +220,18 @@ export const adminRouter = createTRPCRouter({
|
||||||
addPeriods: adminProcedure
|
addPeriods: adminProcedure
|
||||||
.input(AddPeriods)
|
.input(AddPeriods)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await ctx.db.timePeriod.findMany().then(
|
const period = await ctx.db.period.findFirst({
|
||||||
|
where: {
|
||||||
|
date: new Date(input.date),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (period !== null) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Periods with date already exist.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return await ctx.db.timePeriod.findMany().then(
|
||||||
async (timePeriods) => {
|
async (timePeriods) => {
|
||||||
timePeriods.forEach(async (timePeriod) => {
|
timePeriods.forEach(async (timePeriod) => {
|
||||||
await ctx.db.period.create({
|
await ctx.db.period.create({
|
||||||
|
@ -249,6 +260,16 @@ export const adminRouter = createTRPCRouter({
|
||||||
timePeriodId: z.number().int(),
|
timePeriodId: z.number().int(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
// if data exists, do nothing
|
||||||
|
const period = await ctx.db.period.findFirst({
|
||||||
|
where: {
|
||||||
|
date: new Date(input.date),
|
||||||
|
timePeriodId: input.timePeriodId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (period !== null) {
|
||||||
|
return period;
|
||||||
|
}
|
||||||
return await ctx.db.period.create({
|
return await ctx.db.period.create({
|
||||||
data: {
|
data: {
|
||||||
date: new Date(input.date),
|
date: new Date(input.date),
|
||||||
|
@ -257,11 +278,15 @@ export const adminRouter = createTRPCRouter({
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
disablePeriod: adminProcedure
|
disablePeriod: adminProcedure
|
||||||
.input(z.number().int())
|
.input(z.object({
|
||||||
|
date: z.string(),
|
||||||
|
timePeriodId: z.number().int(),
|
||||||
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
return await ctx.db.period.delete({
|
return await ctx.db.period.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: input,
|
date: new Date(input.date),
|
||||||
|
timePeriodId: input.timePeriodId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
@ -284,4 +309,19 @@ export const adminRouter = createTRPCRouter({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
rfidAttendance: adminProcedure
|
||||||
|
.query(() => {
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
rfidAttendance: rfidAttendance,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
toggleRfidAttendance: adminProcedure
|
||||||
|
.mutation(() => {
|
||||||
|
setRfidAttendance(!rfidAttendance);
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
message: "RFID attendance toggled.",
|
||||||
|
};
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -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" },
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
});
|
|
89
src/server/api/routers/time-sel.ts
Normal file
89
src/server/api/routers/time-sel.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createTRPCRouter, loggedInProcedure, publicProcedure } from "~/server/api/trpc";
|
||||||
|
|
||||||
|
export const timeSelRouter = createTRPCRouter({
|
||||||
|
getMySelectedPeriods: loggedInProcedure
|
||||||
|
.query(async ({ ctx }) => {
|
||||||
|
const user = await ctx.db.user.findUnique({
|
||||||
|
where: { username: ctx.session?.username },
|
||||||
|
select: {
|
||||||
|
periods: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!user) throw new TRPCError({ code: "UNAUTHORIZED", message: "User not found" });
|
||||||
|
return user.periods.map(period => period.id);
|
||||||
|
}),
|
||||||
|
toggleAttendance: loggedInProcedure
|
||||||
|
.input(z.object({
|
||||||
|
periodId: z.number().int(),
|
||||||
|
attendance: z.boolean(),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
try {
|
||||||
|
if (input.attendance) {
|
||||||
|
await ctx.db.period.update({
|
||||||
|
where: { id: input.periodId },
|
||||||
|
data: {
|
||||||
|
users: {
|
||||||
|
connect: {
|
||||||
|
username: ctx.session?.username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await ctx.db.period.update({
|
||||||
|
where: { id: input.periodId },
|
||||||
|
data: {
|
||||||
|
users: {
|
||||||
|
disconnect: {
|
||||||
|
username: ctx.session?.username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "An error occurred." });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
attendTime: loggedInProcedure
|
||||||
|
.query(async ({ ctx }) => {
|
||||||
|
const user = await ctx.db.user.findUnique({
|
||||||
|
where: { username: ctx.session?.username },
|
||||||
|
select: {
|
||||||
|
periods: {
|
||||||
|
select: {
|
||||||
|
timePeriod: {
|
||||||
|
select: {
|
||||||
|
start: true,
|
||||||
|
end: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!user) throw new TRPCError({ code: "UNAUTHORIZED", message: "User not found" });
|
||||||
|
let total = 0;
|
||||||
|
user.periods.forEach(period => {
|
||||||
|
const start = new Date();
|
||||||
|
const end = new Date();
|
||||||
|
const [startHour, startMinute] = period.timePeriod.start.split(":");
|
||||||
|
const [endHour, endMinute] = period.timePeriod.end.split(":");
|
||||||
|
start.setHours(parseInt(startHour!));
|
||||||
|
start.setMinutes(parseInt(startMinute!));
|
||||||
|
end.setHours(parseInt(endHour!));
|
||||||
|
end.setMinutes(parseInt(endMinute!));
|
||||||
|
total += (end.getTime() - start.getTime()) / 1000 / 60 / 60; // convert to hours
|
||||||
|
})
|
||||||
|
return total;
|
||||||
|
}),
|
||||||
|
});
|
|
@ -2,3 +2,7 @@ export let lastGotRfid = "";
|
||||||
export const setLastRfid = (uid: string) => {
|
export const setLastRfid = (uid: string) => {
|
||||||
lastGotRfid = uid;
|
lastGotRfid = uid;
|
||||||
};
|
};
|
||||||
|
export let rfidAttendance = true;
|
||||||
|
export const setRfidAttendance = (value: boolean) => {
|
||||||
|
rfidAttendance = value;
|
||||||
|
};
|
Loading…
Reference in a new issue