mirror of
https://github.com/aaronleetw/savage-tracking.git
synced 2024-11-14 11:01:39 -08:00
Made final edits for summer workshop
This commit is contained in:
parent
e02db95f55
commit
a40e993d10
5 changed files with 211 additions and 63 deletions
|
@ -14,9 +14,9 @@ services:
|
|||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/savage_tracking?schema=public
|
||||
JWT_SECRET: secret
|
||||
JWT_SECRET: "CHANGE_THIS"
|
||||
TZ: Asia/Taipei
|
||||
RFID_PKEY: secret
|
||||
RFID_PKEY: "CHANGE_THIS"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
|
|
@ -8,9 +8,10 @@ import { api } from "~/utils/api";
|
|||
export default function Dash() {
|
||||
const { push } = useRouter();
|
||||
const [date, setDate] = useState(new Date());
|
||||
const [genDate, setGenDate] = useState(new Date(0));
|
||||
const [genDate, setGenDate] = useState(new Date());
|
||||
|
||||
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 periods = api.admin.getPeriods.useQuery();
|
||||
|
@ -18,6 +19,8 @@ export default function Dash() {
|
|||
const roster = api.admin.getRoster.useQuery({
|
||||
date: date
|
||||
});
|
||||
const allRoster = api.admin.getAllRoster.useQuery();
|
||||
const allPeriodsHeadcount = api.admin.getAllPeriodsHeadcount.useQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn.failureCount > 0) {
|
||||
|
@ -60,6 +63,16 @@ export default function Dash() {
|
|||
}}>
|
||||
Download PDF
|
||||
</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">
|
||||
<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">
|
||||
<thead>
|
||||
<tr className="*:p-1 *:border border-b-2 border-b-black">
|
||||
<th>學號</th>
|
||||
<th>年級</th>
|
||||
<th>班級</th>
|
||||
<th>座號</th>
|
||||
|
@ -95,6 +109,7 @@ export default function Dash() {
|
|||
{
|
||||
roster.data?.map((user) => {
|
||||
return (<tr key={user.id} className="*:p-1 *:border">
|
||||
<td>{user.username}</td>
|
||||
<td>{user.grade}</td>
|
||||
<td>{user.class}</td>
|
||||
<td>{user.number}</td>
|
||||
|
@ -102,9 +117,9 @@ export default function Dash() {
|
|||
{
|
||||
timePeriods.data?.map((timePeriod) => {
|
||||
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 {
|
||||
return (<td key={timePeriod.id}></td>)
|
||||
return (<td className="text-center" key={timePeriod.id}></td>)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -114,6 +129,73 @@ export default function Dash() {
|
|||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
</main>
|
||||
</>)
|
||||
|
|
|
@ -36,9 +36,9 @@ export default function Dash() {
|
|||
<main className="">
|
||||
<DashboardHeader url="/dash" />
|
||||
<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="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="text-center text-6xl font-bold">{attendTime.data}</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>*/}
|
||||
<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">
|
||||
<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>晚間時段 (晚) 為 16:00 ~ 19:00</li>
|
||||
</ul>
|
||||
<hr className="border-yellow-500" />
|
||||
<ul className="list-disc ml-5 mt-2">
|
||||
<li>1/16/2024 為正常社課時間,不列入時數計算</li>
|
||||
<li>1/17/2024、1/18/2024 開放時段為放學後 17:00 ~ 19:30 (請直接選擇 [晚] 時段)</li>
|
||||
<li>1/19/2024 開放時間為結業式後 12:00 ~ 17:00 (請直接選擇 [午] 時段)</li>
|
||||
<li>1/20/2024 ~ 2/7/2024 開放時間為 09:00 ~ 19:00</li>
|
||||
<li>暑假開放檢修<span className="text-red-500">非強制參加</span>也沒有最低時數限制</li>
|
||||
<li>本系統開放填寫期限為 07/19 18:00</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="text-2xl font-bold mb-4">請點選下方藍色方塊切換狀態 (> 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">
|
||||
<thead>
|
||||
<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}>
|
||||
<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) => {
|
||||
const thisPeriodId = periods.data![date]![periodCnt]?.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>
|
||||
}
|
||||
})
|
||||
) : (
|
||||
timePeriods.data?.map((timePeriod) => {
|
||||
periodCnt++;
|
||||
// ) : (
|
||||
// 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)
|
||||
// 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;
|
||||
}
|
||||
// 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>
|
||||
})
|
||||
)
|
||||
// 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>
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ import { adminProcedure, createTRPCRouter, loggedInProcedure, publicProcedure }
|
|||
import { AddAttendance, AddPeriods, AddTimePeriodSchema, AddUserSchema, ChgPasswordSchema, DateRange, LoginSchema, PublicUserType } from "~/utils/types";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { Period } from "@prisma/client";
|
||||
import { Period, TimePeriod } from "@prisma/client";
|
||||
import { lastGotRfid, rfidAttendance, setRfidAttendance } from "~/utils/rfid";
|
||||
import { actualAttendTime, attendTime as selectedAttendTime, toggleAttendance } from "./time-sel";
|
||||
|
||||
|
@ -200,13 +200,32 @@ export const adminRouter = createTRPCRouter({
|
|||
getPeriods: loggedInProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
return await ctx.db.period.findMany({
|
||||
include: {
|
||||
timePeriod: true
|
||||
},
|
||||
orderBy: [{
|
||||
date: "asc",
|
||||
}, {
|
||||
timePeriodId: "asc",
|
||||
}],
|
||||
}).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) => {
|
||||
const dateStr = period.date.toLocaleDateString();
|
||||
if (groupedPeriods[dateStr] === undefined) {
|
||||
|
@ -402,6 +421,55 @@ export const adminRouter = createTRPCRouter({
|
|||
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
|
||||
.input(z.object({ date: z.date()}))
|
||||
.query(async ({ input, ctx }) => {
|
||||
|
@ -415,6 +483,7 @@ export const adminRouter = createTRPCRouter({
|
|||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
grade: true,
|
||||
class: true,
|
||||
|
|
|
@ -16,11 +16,12 @@ export const attendTime = async (ctx: Context, username: string) => {
|
|||
}
|
||||
}
|
||||
},
|
||||
where: {
|
||||
date: {
|
||||
gte: new Date(new Date().setHours(23, 59, 59, 999)),
|
||||
}
|
||||
}
|
||||
// FIXME: I am temporaily overriding attendance
|
||||
// where: {
|
||||
// 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 (checkWeekInAdvance) {
|
||||
const weekInAdvance = new Date("2024/01/16 23:59");
|
||||
if (new Date() > weekInAdvance) throw new TRPCError({ code: "BAD_REQUEST", message: "Time selection period has passed. Please contact HR." });
|
||||
const weekInAdvance = new Date("2024/07/19 18:00");
|
||||
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) {
|
||||
|
|
Loading…
Reference in a new issue