From 7d0b32382b572af5d7ca0608d28b29c1c9e60f6b Mon Sep 17 00:00:00 2001 From: Aaron Lee Date: Mon, 1 Jan 2024 00:33:21 +0800 Subject: [PATCH] Complete app --- package.json | 2 + src/components/DashboardHeader.tsx | 31 +- src/components/admin/Config.tsx | 5 +- src/components/admin/Periods.tsx | 2 +- src/components/admin/Users.tsx | 37 ++- src/components/admin/attendance/ListView.tsx | 119 ++++++++ src/components/admin/attendance/UserList.tsx | 39 +++ src/components/admin/attendance/UserView.tsx | 244 ++++++++++++++++ src/pages/api/rfid.ts | 2 +- src/pages/dash/admin/attendance.tsx | 53 ++++ src/pages/dash/admin/index.tsx | 2 +- src/pages/dash/admin/rfid.tsx | 3 +- src/pages/dash/index.tsx | 37 ++- src/server/api/routers/admin.ts | 217 +++++++++++++- src/server/api/routers/time-sel.ts | 286 ++++++++++--------- src/server/api/trpc.ts | 2 + src/utils/types.ts | 12 +- yarn.lock | 12 + 18 files changed, 919 insertions(+), 186 deletions(-) create mode 100644 src/components/admin/attendance/ListView.tsx create mode 100644 src/components/admin/attendance/UserList.tsx create mode 100644 src/components/admin/attendance/UserView.tsx create mode 100644 src/pages/dash/admin/attendance.tsx diff --git a/package.json b/package.json index d4c4fc9..9165ccd 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,11 @@ "@trpc/server": "^10.43.6", "@types/bcrypt": "^5.0.2", "@types/jsonwebtoken": "^9.0.5", + "@types/papaparse": "^5.3.14", "bcrypt": "^5.1.1", "jsonwebtoken": "^9.0.2", "next": "^14.0.3", + "papaparse": "^5.4.1", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.49.2", diff --git a/src/components/DashboardHeader.tsx b/src/components/DashboardHeader.tsx index d335fb6..316b1c0 100644 --- a/src/components/DashboardHeader.tsx +++ b/src/components/DashboardHeader.tsx @@ -15,7 +15,7 @@ export default function DashboardHeader({ url }: { url: string }) { }, [logout.isSuccess]) useEffect(() => { - if (session.data?.rosterOnly && !url.startsWith("/dash/admin/roster")) + if (session.data?.rosterOnly && !(url.startsWith("/dash/admin/roster") || url.startsWith("/dash/admin/attendance") || url.startsWith("/dash/chgPassword"))) push("/dash/admin/roster"); }, [session.data]) @@ -34,7 +34,7 @@ export default function DashboardHeader({ url }: { url: string }) {
{ - !session?.data?.rosterOnly && (<> + !session?.data?.rosterOnly && ( - - - - - ) + ) } + + + { session?.data?.isAdmin && ( <> @@ -64,6 +63,14 @@ export default function DashboardHeader({ url }: { url: string }) { Roster + + + { !session?.data?.rosterOnly && (<> diff --git a/src/components/admin/Config.tsx b/src/components/admin/Config.tsx index 8d7f475..26fc9f3 100644 --- a/src/components/admin/Config.tsx +++ b/src/components/admin/Config.tsx @@ -1,4 +1,5 @@ import { zodResolver } from "@hookform/resolvers/zod"; +import { useState } from "react"; import { SubmitHandler, useForm } from "react-hook-form"; import { z } from "zod"; import { api } from "~/utils/api"; @@ -56,10 +57,10 @@ export default function Config() { - + - + diff --git a/src/components/admin/Periods.tsx b/src/components/admin/Periods.tsx index 641d25f..fc1e0a1 100644 --- a/src/components/admin/Periods.tsx +++ b/src/components/admin/Periods.tsx @@ -83,7 +83,7 @@ export default function Periods() {
- + {errors.date && {errors.date.message}}
+ { + (attendanceData.isLoading) &&
+ + Loading... +
+ } +
+ + + + + + + + + + + + + + { + + attendanceData.data?.map((attendance) => { + return ( + + + + + + + + ) + }) + } + +
TimeUsernameGradeClassNumberNameOperation
{attendance.datetime.toLocaleString()}{attendance.user.username}{attendance.user.grade}{attendance.user.class}{attendance.user.number}{attendance.user.name} + +
+

Create new record

+ +
+ + + +
+ {addAttendanceRecord.isLoading &&

Loading...

} + {addAttendanceRecord.isSuccess &&

Success!

} + {addAttendanceRecord.isError &&

{addAttendanceRecord.error?.message}

} + +
+} \ No newline at end of file diff --git a/src/components/admin/attendance/UserList.tsx b/src/components/admin/attendance/UserList.tsx new file mode 100644 index 0000000..bb755a2 --- /dev/null +++ b/src/components/admin/attendance/UserList.tsx @@ -0,0 +1,39 @@ +import { api } from "~/utils/api"; + +export default function UserList() { + const users = api.admin.getAllUsersAttendTime.useQuery(); + + return + + + + + + + + + + + + + + + { + users.data?.map((user) => ( + = 30 ? "*:bg-emerald-200" : "*:bg-red-100"}`} key={user.username}> + + + + + + + + + + + )) + } + +
UsernameGradeClassNumberNameDisplay NameSelected TimeActual TimeProjected Time
{user.username}{user.grade}{user.class}{user.number}{user.name}{user.dname}{user.selectedTime}{user.actualTime.toFixed(2)}{(user.selectedTime + user.actualTime).toFixed(2)}
+ +} \ No newline at end of file diff --git a/src/components/admin/attendance/UserView.tsx b/src/components/admin/attendance/UserView.tsx new file mode 100644 index 0000000..d0d6678 --- /dev/null +++ b/src/components/admin/attendance/UserView.tsx @@ -0,0 +1,244 @@ +import { useState } from "react"; +import { api } from "~/utils/api"; + +export default function UserView() { + const [username, setUsername] = useState(""); + + const periods = api.admin.getPeriods.useQuery(); + const timePeriods = api.admin.getTimePeriods.useQuery(); + const userSelectedPeriods = api.admin.getUserSelectedPeriods.useQuery(username); + const userAttendance = api.admin.getUserAttendance.useQuery(username); + const userData = api.admin.getUserData.useQuery(username); + const userToggleAttendance = api.admin.userToggleAttendance.useMutation(); + const userAttendTime = api.admin.userAttendTime.useQuery(username); + const userSelectedAllAttendTime = api.admin.userSelectedAllAttendTime.useQuery(username); + const userActualAttendTime = api.admin.userActualAttendTime.useQuery(username); + let periodCnt = 0; + let attCnt = 0; + let entered = false; + let passedDate = false; + + return
+
+ + + + + + + + { + (userSelectedPeriods.isLoading || userData.isLoading) &&
+ + Loading... +
+ } +
+ {userData.data === null &&
User not found.
} +
+ {userData.data !== null && userData.data !== undefined && <> +
+
+
總選取時數
+
+
{userSelectedAllAttendTime.data}
+
+
+
+
未來選取時數
+
+
{userAttendTime.data}
+
+
+
+
實際出席時數
+
+
{userActualAttendTime.data?.toFixed(1)}
+
+
+
= 30 ? "bg-emerald-500" : "bg-red-300" + ].join(" ")}> +
預估總時數
+
+
{((userActualAttendTime.data || 0) + (userAttendTime.data || 0)).toFixed(1)}
+
+
+
+ + + + + + { + timePeriods.data?.map((timePeriods) => { + return () + }) + } + + + + { + Object.keys(periods.data || {}).map((date) => { + periodCnt = 0; + const borderTop = new Date(date).setHours(0, 0, 0, 0) > new Date().getTime() && !passedDate; + if (borderTop) passedDate = true; + return ( + + + { + timePeriods.data?.map((timePeriod) => { + const thisPeriodId = periods.data![date]![periodCnt]?.id!; + if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) { + periodCnt++; + if (userSelectedPeriods.data?.findIndex((period) => period == thisPeriodId) != -1) { + return + } else { + return + } + } else { + return + } + }) + } + + ) + }) + } + +
Date{timePeriods.name} ({timePeriods.start} ~ {timePeriods.end})
{date} userToggleAttendance.mutateAsync({ + username: username, + periodId: thisPeriodId || -1, + attendance: false + }).then(() => { + userSelectedPeriods.refetch(); + userAttendTime.refetch(); + userSelectedAllAttendTime.refetch(); + }).catch((e) => alert(e.message))}> + Will Attend + userToggleAttendance.mutateAsync({ + username: username, + periodId: thisPeriodId || -1, + attendance: true + }).then(() => { + userSelectedPeriods.refetch(); + userAttendTime.refetch(); + userSelectedAllAttendTime.refetch(); + }).catch((e) => alert(e.message))}>N/A
+ + + + + { + timePeriods.data?.map((timePeriods) => { + return () + }) + } + + + + { + Object.keys(periods.data || {}).map((date) => { + periodCnt = 0; + entered = false; + const borderTop = new Date(date).setHours(0, 0, 0, 0) > new Date().getTime() && !passedDate; + if (borderTop) passedDate = true; + 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 < (userAttendance.data || []).length; i++) { + const thisAtt = userAttendance.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 + }) + } + + ) + }) + } + +
Date{timePeriods.name} ({timePeriods.start} ~ {timePeriods.end})
{date}{data}Absent{data}
+ } +
+} diff --git a/src/pages/api/rfid.ts b/src/pages/api/rfid.ts index 5dde305..78f30fb 100644 --- a/src/pages/api/rfid.ts +++ b/src/pages/api/rfid.ts @@ -36,7 +36,7 @@ export default async function handler( return; } } else { - res.status(200).json({ status: 'RFID_ATTENDANCE_NOT_ENABLED' }); + res.status(202).json({ status: 'RFID_ATTENDANCE_NOT_ENABLED' }); } } else { diff --git a/src/pages/dash/admin/attendance.tsx b/src/pages/dash/admin/attendance.tsx new file mode 100644 index 0000000..b90958b --- /dev/null +++ b/src/pages/dash/admin/attendance.tsx @@ -0,0 +1,53 @@ +import Head from "next/head"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import DashboardHeader from "~/components/DashboardHeader"; +import ListView from "~/components/admin/attendance/ListView"; +import UserList from "~/components/admin/attendance/UserList"; +import UserView from "~/components/admin/attendance/UserView"; +import { api } from "~/utils/api"; + +export default function Dash() { + const { push } = useRouter(); + const isLoggedIn = api.admin.isLoggedIn.useQuery(); + + const [currentPage, setCurrentPage] = useState("list"); + + useEffect(() => { + if (isLoggedIn.failureCount > 0) { + push("/"); + } + }, [isLoggedIn.failureCount]) + + if (isLoggedIn.isLoading) return <> + + return (<> + + Savage Tracking + + + +
+ +
+ + { currentPage === "list" && } + { currentPage === "user" && } + { currentPage === "userlist" && } +
+
+ ) +} \ No newline at end of file diff --git a/src/pages/dash/admin/index.tsx b/src/pages/dash/admin/index.tsx index 5905a66..70bd0bd 100644 --- a/src/pages/dash/admin/index.tsx +++ b/src/pages/dash/admin/index.tsx @@ -30,7 +30,7 @@ export default function Dash() {
-