Made final edits for summer workshop

This commit is contained in:
Aaron Lee 2024-08-14 02:59:05 -07:00
parent e02db95f55
commit a40e993d10
5 changed files with 211 additions and 63 deletions

View file

@ -14,9 +14,9 @@ services:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/savage_tracking?schema=public DATABASE_URL: postgresql://postgres:postgres@postgres:5432/savage_tracking?schema=public
JWT_SECRET: secret JWT_SECRET: "CHANGE_THIS"
TZ: Asia/Taipei TZ: Asia/Taipei
RFID_PKEY: secret RFID_PKEY: "CHANGE_THIS"
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy

View file

@ -8,9 +8,10 @@ import { api } from "~/utils/api";
export default function Dash() { export default function Dash() {
const { push } = useRouter(); const { push } = useRouter();
const [date, setDate] = useState(new Date()); const [date, setDate] = useState(new Date());
const [genDate, setGenDate] = useState(new Date(0)); const [genDate, setGenDate] = useState(new Date());
const { toPDF, targetRef: pdfRef } = usePDF({ filename: `入校名單_${date.toLocaleDateString().replace("/", "-")}.pdf` }); const { toPDF, targetRef: pdfRef } = usePDF({ filename: `入校名單_${date.toLocaleDateString().replace("/", "-")}.pdf` });
const { toPDF: bigToPdf, targetRef: bigPdfRef } = usePDF({ filename: `Build_Season_總表.pdf` });
const isLoggedIn = api.admin.isLoggedIn.useQuery(); const isLoggedIn = api.admin.isLoggedIn.useQuery();
const periods = api.admin.getPeriods.useQuery(); const periods = api.admin.getPeriods.useQuery();
@ -18,6 +19,8 @@ export default function Dash() {
const roster = api.admin.getRoster.useQuery({ const roster = api.admin.getRoster.useQuery({
date: date date: date
}); });
const allRoster = api.admin.getAllRoster.useQuery();
const allPeriodsHeadcount = api.admin.getAllPeriodsHeadcount.useQuery();
useEffect(() => { useEffect(() => {
if (isLoggedIn.failureCount > 0) { if (isLoggedIn.failureCount > 0) {
@ -60,6 +63,16 @@ export default function Dash() {
}}> }}>
Download PDF Download PDF
</button> </button>
<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={() => {
bigToPdf({
page: {
orientation: "landscape"
}
});
}}>
Download PDF
</button>
{ {
(roster.isLoading) && <div role="status"> (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"> <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">
@ -80,6 +93,7 @@ export default function Dash() {
<table className="table-auto border-collapse border-2 border-black w-fit mt-3 font-kai text-lg"> <table className="table-auto border-collapse border-2 border-black w-fit mt-3 font-kai text-lg">
<thead> <thead>
<tr className="*:p-1 *:border border-b-2 border-b-black"> <tr className="*:p-1 *:border border-b-2 border-b-black">
<th></th>
<th></th> <th></th>
<th></th> <th></th>
<th></th> <th></th>
@ -95,6 +109,7 @@ export default function Dash() {
{ {
roster.data?.map((user) => { roster.data?.map((user) => {
return (<tr key={user.id} className="*:p-1 *:border"> return (<tr key={user.id} className="*:p-1 *:border">
<td>{user.username}</td>
<td>{user.grade}</td> <td>{user.grade}</td>
<td>{user.class}</td> <td>{user.class}</td>
<td>{user.number}</td> <td>{user.number}</td>
@ -102,9 +117,9 @@ export default function Dash() {
{ {
timePeriods.data?.map((timePeriod) => { timePeriods.data?.map((timePeriod) => {
if (user.periods.find((period) => period.timePeriod.id === timePeriod.id)) { if (user.periods.find((period) => period.timePeriod.id === timePeriod.id)) {
return (<td key={timePeriod.id}></td>) return (<td className="text-center" key={timePeriod.id}></td>)
} else { } else {
return (<td key={timePeriod.id}></td>) return (<td className="text-center" key={timePeriod.id}></td>)
} }
}) })
} }
@ -114,6 +129,73 @@ export default function Dash() {
</tbody> </tbody>
</table> </table>
</div> </div>
<hr />
<div className="pdf my-5 w-fit p-5" ref={bigPdfRef}>
<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">
<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">
<th rowSpan={2}></th>
<th rowSpan={2}></th>
<th rowSpan={2}></th>
<th rowSpan={2}></th>
{
Object.keys(periods.data ?? {}).map((date) => {
return (<th key={date} colSpan={
periods.data![date]!.length
} className="w-24">{date}</th>)
})
}
</tr>
<tr className="*:p-1 *:border border-b-2 border-b-black">
{
Object.keys(periods.data ?? {}).map((date) => {
return periods.data![date]!.map((period) => {
return (<th key={period.id}>{period.timePeriod.name}</th>)
})
})
}
</tr>
</thead>
<tbody>
{
allRoster.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>
{
Object.keys(periods.data ?? {}).map((date) => {
return periods.data![date]!.map((thisPeriod) => {
if (user.periods.find((period) => period.id === thisPeriod.id)) {
return (<td className="text-center" key={thisPeriod.id * user.id}></td>)
} else {
return (<td className="text-center" key={thisPeriod.id * user.id}></td>)
}
})
})
}
</tr>)
})
}
</tbody>
<tfoot>
<tr className="*:p-1 *:border">
<td colSpan={4}></td>
{
allPeriodsHeadcount.data?.map((headcount) => {
return (<td key={headcount.id}>{headcount._count.users}</td>)
})
}
</tr>
</tfoot>
</table>
</div>
</div> </div>
</main> </main>
</>) </>)

View file

@ -36,9 +36,9 @@ export default function Dash() {
<main className=""> <main className="">
<DashboardHeader url="/dash" /> <DashboardHeader url="/dash" />
<div className="p-5"> <div className="p-5">
<div className="flex gap-2"> {/*<div className="flex gap-2">
<div className="border rounded-xl h-32 w-40 p-2 flex items-center justify-center flex-col mb-4 bg-gray-300"> <div className="border rounded-xl h-32 w-40 p-2 flex items-center justify-center flex-col mb-4 bg-gray-300">
<div className="text-center text-2xl font-bold"></div> <div className="text-center text-2xl font-bold"></div>
<div className="flex-grow flex items-center"> <div className="flex-grow flex items-center">
<div className="text-center text-6xl font-bold">{attendTime.data}</div> <div className="text-center text-6xl font-bold">{attendTime.data}</div>
</div> </div>
@ -58,25 +58,20 @@ export default function Dash() {
<div className="text-center text-6xl font-bold">{((actualAttendTime.data ?? 0) + (attendTime.data ?? 0)).toFixed(1)}</div> <div className="text-center text-6xl font-bold">{((actualAttendTime.data ?? 0) + (attendTime.data ?? 0)).toFixed(1)}</div>
</div> </div>
</div> </div>
</div> </div>*/}
<div className="p-2 mb-4 border border-yellow-500 bg-yellow-100 max-w-full w-[40rem] rounded-md"> <div className="p-2 mb-4 border border-yellow-500 bg-yellow-100 max-w-full w-[40rem] rounded-md">
<ul className="list-disc ml-5 mb-2"> <ul className="list-disc ml-5 mb-2">
<li> () 09:00 ~ 12:00</li> <li> () 09:00 ~ 12:00</li>
<ul className="list-[circle] ml-5 text-red-500">
<li> 3 4 </li>
</ul>
<li> () 13:00 ~ 16:00</li> <li> () 13:00 ~ 16:00</li>
<li> () 16:00 ~ 19:00</li> <li> () 16:00 ~ 19:00</li>
</ul> </ul>
<hr className="border-yellow-500" /> <hr className="border-yellow-500" />
<ul className="list-disc ml-5 mt-2"> <ul className="list-disc ml-5 mt-2">
<li>1/16/2024 </li> <li><span className="text-red-500"></span></li>
<li>1/17/20241/18/2024 17:00 ~ 19:30 ( [] )</li> <li> 07/19 18:00</li>
<li>1/19/2024 12:00 ~ 17:00 ( [] )</li>
<li>1/20/2024 ~ 2/7/2024 09:00 ~ 19:00</li>
</ul> </ul>
</div> </div>
<div className="text-2xl font-bold mb-4"> (&gt; 95 )</div> <div className="text-2xl font-bold mb-4"></div>
<table className="table-auto border-collapse border-2 border-black w-fit mb-5"> <table className="table-auto border-collapse border-2 border-black w-fit mb-5">
<thead> <thead>
<tr className="*:p-1 *:border border-b-2 border-b-black"> <tr className="*:p-1 *:border border-b-2 border-b-black">
@ -97,7 +92,8 @@ export default function Dash() {
<tr className="*:p-1 *:border" key={date}> <tr className="*:p-1 *:border" key={date}>
<td>{date}</td> <td>{date}</td>
{ {
new Date(date).setHours(0, 0, 0, 0) > new Date().getTime() ? ( // FIXME: I am temporaily overriding attendance
// new Date(date).setHours(0, 0, 0, 0) > new Date().getTime() ? (
timePeriods.data?.map((timePeriod) => { timePeriods.data?.map((timePeriod) => {
const thisPeriodId = periods.data![date]![periodCnt]?.id!; const thisPeriodId = periods.data![date]![periodCnt]?.id!;
if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) { if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) {
@ -121,43 +117,43 @@ export default function Dash() {
return <td key={timePeriod.id * periodCnt} className="bg-gray-400">N/A</td> return <td key={timePeriod.id * periodCnt} className="bg-gray-400">N/A</td>
} }
}) })
) : ( // ) : (
timePeriods.data?.map((timePeriod) => { // timePeriods.data?.map((timePeriod) => {
periodCnt++; // periodCnt++;
let data = ""; // let data = "";
const thisPeriodStart = new Date(date); // const thisPeriodStart = new Date(date);
thisPeriodStart.setHours(parseInt(timePeriod.start.split(":")[0]!), parseInt(timePeriod.start.split(":")[1]!), 0, 0) // thisPeriodStart.setHours(parseInt(timePeriod.start.split(":")[0]!), parseInt(timePeriod.start.split(":")[1]!), 0, 0)
const thisPeriodEnd = new Date(date); // const thisPeriodEnd = new Date(date);
thisPeriodEnd.setHours(parseInt(timePeriod.end.split(":")[0]!), parseInt(timePeriod.end.split(":")[1]!), 0, 0) // thisPeriodEnd.setHours(parseInt(timePeriod.end.split(":")[0]!), parseInt(timePeriod.end.split(":")[1]!), 0, 0)
for (let i = attCnt; i < (myAttendance.data ?? []).length; i++) { // for (let i = attCnt; i < (myAttendance.data ?? []).length; i++) {
const thisAtt = myAttendance.data![i]; // const thisAtt = myAttendance.data![i];
if (thisAtt?.datetime! < thisPeriodStart) continue; // if (thisAtt?.datetime! < thisPeriodStart) continue;
if (thisAtt?.datetime! > thisPeriodEnd) { // if (thisAtt?.datetime! > thisPeriodEnd) {
attCnt = i; // attCnt = i;
break; // break;
} // }
if (!entered) { // if (!entered) {
data += `${data !== "" ? " / " : ""}${thisAtt?.datetime!.toLocaleTimeString()} ~ `; // data += `${data !== "" ? " / " : ""}${thisAtt?.datetime!.toLocaleTimeString()} ~ `;
} else { // } else {
data += `${thisAtt?.datetime!.toLocaleTimeString()}`; // data += `${thisAtt?.datetime!.toLocaleTimeString()}`;
} // }
entered = !entered; // entered = !entered;
} // }
if (entered && data === "" && periodCnt !== timePeriods.data!.length) { // if (entered && data === "" && periodCnt !== timePeriods.data!.length) {
return <td key={timePeriod.id * periodCnt} className="bg-green-700 text-white"></td> // return <td key={timePeriod.id * periodCnt} className="bg-green-700 text-white"></td>
} // }
if (entered && periodCnt === timePeriods.data!.length) { // if (entered && periodCnt === timePeriods.data!.length) {
return <td key={timePeriod.id * periodCnt} className="bg-yellow-700 text-white">{data}</td> // return <td key={timePeriod.id * periodCnt} className="bg-yellow-700 text-white">{data}</td>
} // }
if (data === "") { // if (data === "") {
return <td key={timePeriod.id * periodCnt} className="bg-gray-500 text-white">Absent</td> // 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> // return <td key={timePeriod.id * periodCnt} className="bg-green-700 text-white">{data}</td>
}) // })
) // )
} }
</tr> </tr>
) )

View file

@ -5,7 +5,7 @@ import { adminProcedure, createTRPCRouter, loggedInProcedure, publicProcedure }
import { AddAttendance, AddPeriods, AddTimePeriodSchema, AddUserSchema, ChgPasswordSchema, DateRange, LoginSchema, PublicUserType } from "~/utils/types"; import { AddAttendance, AddPeriods, AddTimePeriodSchema, AddUserSchema, ChgPasswordSchema, DateRange, LoginSchema, PublicUserType } from "~/utils/types";
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, TimePeriod } from "@prisma/client";
import { lastGotRfid, rfidAttendance, setRfidAttendance } from "~/utils/rfid"; import { lastGotRfid, rfidAttendance, setRfidAttendance } from "~/utils/rfid";
import { actualAttendTime, attendTime as selectedAttendTime, toggleAttendance } from "./time-sel"; import { actualAttendTime, attendTime as selectedAttendTime, toggleAttendance } from "./time-sel";
@ -200,13 +200,32 @@ export const adminRouter = createTRPCRouter({
getPeriods: loggedInProcedure getPeriods: loggedInProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }) => {
return await ctx.db.period.findMany({ return await ctx.db.period.findMany({
include: {
timePeriod: true
},
orderBy: [{ orderBy: [{
date: "asc", date: "asc",
}, { }, {
timePeriodId: "asc", timePeriodId: "asc",
}], }],
}).then((periods) => { }).then((periods) => {
const groupedPeriods: { [key: string]: Period[] } = {}; const groupedPeriods: { [key: string]: ({
timePeriod: {
id: number;
name: string;
start: string;
end: string;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
date: Date;
timePeriodId: number;
collecting: boolean;
createdAt: Date;
updatedAt: Date;
})[]} = {};
periods.forEach((period) => { periods.forEach((period) => {
const dateStr = period.date.toLocaleDateString(); const dateStr = period.date.toLocaleDateString();
if (groupedPeriods[dateStr] === undefined) { if (groupedPeriods[dateStr] === undefined) {
@ -402,6 +421,55 @@ export const adminRouter = createTRPCRouter({
message: "RFID attendance toggled.", message: "RFID attendance toggled.",
}; };
}), }),
getAllRoster: adminProcedure
.query(async ({ ctx }) => {
return await ctx.db.user.findMany({
select: {
id: true,
name: true,
grade: true,
class: true,
number: true,
periods: {
select: {
id: true,
},
},
},
orderBy: [
{
grade: "asc",
},
{
class: "asc",
},
{
number: "asc",
},
]
})
}),
getAllPeriodsHeadcount: adminProcedure
.query(async ({ ctx }) => {
return await ctx.db.period.findMany({
select: {
id: true,
_count: {
select: {
users: true,
}
}
},
orderBy: [
{
date: "asc",
},
{
timePeriodId: "asc",
},
]
})
}),
getRoster: adminProcedure getRoster: adminProcedure
.input(z.object({ date: z.date()})) .input(z.object({ date: z.date()}))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
@ -415,6 +483,7 @@ export const adminRouter = createTRPCRouter({
}, },
select: { select: {
id: true, id: true,
username: true,
name: true, name: true,
grade: true, grade: true,
class: true, class: true,

View file

@ -16,11 +16,12 @@ export const attendTime = async (ctx: Context, username: string) => {
} }
} }
}, },
where: { // FIXME: I am temporaily overriding attendance
date: { // where: {
gte: new Date(new Date().setHours(23, 59, 59, 999)), // date: {
} // gte: new Date(new Date().setHours(23, 59, 59, 999)),
} // }
// }
} }
} }
}); });
@ -119,8 +120,8 @@ export const toggleAttendance = async (ctx: Context, input: { periodId: number,
if (!period) throw new TRPCError({ code: "NOT_FOUND", message: "Period not found" }); if (!period) throw new TRPCError({ code: "NOT_FOUND", message: "Period not found" });
if (checkWeekInAdvance) { if (checkWeekInAdvance) {
const weekInAdvance = new Date("2024/01/16 23:59"); const weekInAdvance = new Date("2024/07/19 18:00");
if (new Date() > weekInAdvance) throw new TRPCError({ code: "BAD_REQUEST", message: "Time selection period has passed. Please contact HR." }); if (new Date() > weekInAdvance || period.date < weekInAdvance) throw new TRPCError({ code: "BAD_REQUEST", message: "Time selection period has passed. Please contact HR." });
} }
if (input.attendance) { if (input.attendance) {