Skip to content

Commit

Permalink
feat: add attendance grid
Browse files Browse the repository at this point in the history
  • Loading branch information
rutmanz committed Sep 18, 2024
1 parent 4ab2458 commit 9e73816
Show file tree
Hide file tree
Showing 11 changed files with 518 additions and 4 deletions.
1 change: 1 addition & 0 deletions esbuild-frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ fs.stat('./public').catch(async () => {

const views: Record<string, string> = {
grid: '/grid/',
grid_attendance: '/attendance/',
dash: '/dash/',
admin_members: '/admin/members/',
admin_certs: '/admin/certs/',
Expand Down
9 changes: 9 additions & 0 deletions src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,12 @@ export const requireAdminLogin: MiddlewareHandler<BlankEnv, never, object> = asy
return c.redirect(`/auth/login?redirectTo=${c.req.path}&level=admin`, 302)
}
}
export const requireAdminAPI: MiddlewareHandler<BlankEnv, never, object> = async (c, next) => {
await validateAuth(c)
if (c.get('auth_admin')) {
await next()
} else {
c.status(401)
return c.text('Invalid key')
}
}
13 changes: 12 additions & 1 deletion src/routes/api/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import prisma from '~lib/prisma'
import logger from '~lib/logger'
import { safeParseInt } from '~lib/util'
import { season_start_date } from '~config'
import { requireAdminLogin } from '~lib/auth'

export const router = new Hono()

router.use(requireAdminLogin)
router
.get('/admin/members', async (c) => {
return c.json(
Expand Down Expand Up @@ -125,3 +126,13 @@ router
return c.json({ error: err }, 400)
}
})

router.post('/admin/meetings/create', async (c) => {
await prisma.meetings.create({
data: {
date: new Date(),
mandatory: true
}
})
return c.json({ success: true })
})
61 changes: 59 additions & 2 deletions src/routes/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Context, Hono } from 'hono'
import { syncSlackMembers } from '~tasks/slack'
import { APIClockLabRequest, APIClockResponse, APIMember } from '~types'
import { APIClockLabRequest, APIClockResponse, APIMeetingAttendance, APIMember, APIRoutes } from '~types'
import logger from '~lib/logger'
import { requireReadAPI, requireWriteAPI } from '~lib/auth'
import { requireAdminAPI, requireAdminLogin, requireReadAPI, requireWriteAPI } from '~lib/auth'

Check warning on line 5 in src/routes/api/index.ts

View workflow job for this annotation

GitHub Actions / Lint

'requireAdminAPI' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 5 in src/routes/api/index.ts

View workflow job for this annotation

GitHub Actions / Lint

'requireAdminLogin' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 5 in src/routes/api/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

src/routes/api/index.ts#L5

[@typescript-eslint/no-unused-vars] 'requireAdminAPI' is defined but never used. Allowed unused vars must match /^_/u.

Check warning on line 5 in src/routes/api/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

src/routes/api/index.ts#L5

[@typescript-eslint/no-unused-vars] 'requireAdminLogin' is defined but never used. Allowed unused vars must match /^_/u.
import { emitCluckChange } from '~lib/sockets'
import prisma, { getMemberPhotoOrDefault } from '~lib/prisma'
import { cors } from 'hono/cors'
import { completeHourLog } from '~lib/hour_operations'
import { router as admin_api_router } from './admin'
import { router as dash_api_router } from './dash'
import { enum_MeetingAttendances_state } from '@prisma/client'

const router = new Hono()
router.route('/', admin_api_router)
Expand Down Expand Up @@ -126,4 +127,60 @@ router
return c.json(records.map(({ id, member_id, time_in }) => ({ id, time_in, email: member_id })))
})

router
.post('/attendance', requireWriteAPI, async (c) => {
const { email, state, meeting }: APIMeetingAttendance = await c.req.json()
try {
await prisma.meetingAttendanceEntry.upsert({
where: {
meeting_id_member_id: {
meeting_id: meeting,
member_id: email
}
},
create: {
meeting_id: meeting,
member_id: email,
state
},
update: {
state
}
})
return c.json({ success: true, error: null })
} catch (e) {
logger.error(e, 'POST /attendance failed')
c.status(500)
return c.json({ success: false, error: e })
}
})
.get(requireWriteAPI, async (c) => {
const meeting = await prisma.meetings.findFirst({
orderBy: {
createdAt: 'desc'
},
select: {
id: true,
date: true,
Attendances: {
select: {
state: true,
member_id: true
}
}
}
})
if (!meeting) {
c.status(400)
return c.text('No meetings found')
}
const map: Record<string, enum_MeetingAttendances_state> = {}
meeting.Attendances.forEach((a) => (map[a.member_id] = a.state))
const out: APIRoutes['/attendance']['GET']['resp'] = {
id: meeting.id,
label: meeting.date.toLocaleDateString(),
attendance: map
}
return c.json(out)
})
export default router
18 changes: 18 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { enum_MeetingAttendances_state } from '@prisma/client'
import { PostItem } from '~routes/api/dash'

export type HourCategory = 'lab' | 'external' | 'summer' | 'event' // must also change enum constraint when modifying
Expand Down Expand Up @@ -29,6 +30,12 @@ export type APIClockExternalRespondRequest = {
}
export type APIClockResponse = { success: false; error: string; log_id?: number } | { success: true; log_id: number }

export type APIMeetingAttendance = {
email: string
state: enum_MeetingAttendances_state
meeting: number
}

type APIMembersResponse = APIMember[]
export type APILoggedIn = { id: string; email: string; time_in: string }

Expand All @@ -47,6 +54,17 @@ export interface APIRoutes extends Record<string, APIRoute> {
POST: { req: APIClockLabRequest; resp: APIClockResponse }
GET: { req: null; resp: APILoggedIn[] }
}
'/attendance': {
POST: { req: APIMeetingAttendance; resp: APIClockResponse }
GET: {
req: null
resp: {
id: number
label: string
attendance: Record<string, enum_MeetingAttendances_state>
}
}
}
'/members': {
GET: { req: null; resp: APIMembersResponse }
}
Expand Down
6 changes: 5 additions & 1 deletion src/views/admin_meetings/index.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
<!doctype html>
<html lang="en-us">
<head>
<title>Certs</title>
<title>Meetings</title>
<link rel="stylesheet" href="/static/app.css" />
<!-- <link rel="shortcut icon" href="../favicon.ico" type="image/x-icon">-->
</head>

<body class="bg-gray-800">
<div class="flex flex-col p-4 gap-4 absolute top-0 bottom-0 left-0 right-0">
<div id="mygrid" class="ag-theme-quartz-dark" style="height: 100%"></div>
<div class="flex flex-row gap-4">
<button class="border-green-400 text-sm text-green-200 hover:text-green-50 px-2 py-1 rounded-md border bg-transparent hover:bg-green-500 transition-colors duration-300" id="btn-create">Create Meeting</button>
</div>
</div>

<script src="./index.ts"></script>
</body>
</html>
8 changes: 8 additions & 0 deletions src/views/admin_meetings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,13 @@ async function main() {
const meetings: Prisma.Meetings[] = await resp.json()
gridApi.setGridOption('rowData', meetings)
})

document.getElementById('btn-create')?.addEventListener('click', async () => {
await fetch('/api/admin/meetings/create', { method: 'POST' })
fetch('/api/admin/meetings').then(async (resp) => {
const meetings: Prisma.Meetings[] = await resp.json()
gridApi.setGridOption('rowData', meetings)
})
})
}
main()
60 changes: 60 additions & 0 deletions src/views/grid_attendance/clockapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/* exported clock cluckedIn ping refreshMemberList getApiKey checkAuth*/

import type { APIClockLabRequest, APIMember, APILoggedIn, APIMeetingAttendance } from '~types'
import { apiFetch } from '~views/util'
import { getClockMode } from '~views/grid/style'
import { enum_MeetingAttendances_state } from '@prisma/client'

export type MemberState = enum_MeetingAttendances_state

export async function clock(email: string, clockingIn: boolean): Promise<boolean> {
const outMode = getClockMode() == 'normal' ? 'out' : 'void'
const body: APIClockLabRequest = {
email: email,
action: clockingIn ? 'in' : outMode
}
const res = await apiFetch('/clock/lab', 'POST', body)
return res?.success ?? false
}

export async function setAttendance(email: string, state: MemberState): Promise<boolean> {
const body: APIMeetingAttendance = {
meeting: window.meeting_id,
email: email,
state: state
}
const res = await apiFetch('/attendance', 'POST', body)
return res?.success ?? false
}

export async function getAttendance() {
const res = await apiFetch('/attendance', 'GET', null)
if (!res) {
throw new Error('error loading data')
}
return res
}

export async function getLoggedIn(): Promise<APILoggedIn[]> {
const res = await apiFetch('/clock/lab', 'GET', null)
if (!res) {
throw new Error('error loading data')
}
return res
}

export async function refreshMemberList(): Promise<APIMember[]> {
const res = await apiFetch('/members/refresh', 'GET', null)
if (!res) {
throw new Error('error loading data')
}
return res
}

export async function getMemberList(): Promise<APIMember[]> {
const res = await apiFetch('/members', 'GET', null)
if (!res) {
throw new Error('error loading data')
}
return res
}
16 changes: 16 additions & 0 deletions src/views/grid_attendance/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en-us">
<head>
<title>Cluck Grid</title>
<link rel="stylesheet" href="./style.scss" />
<!-- <link rel="shortcut icon" href="../favicon.ico" type="image/x-icon">-->
</head>

<body>
<div id="title">December 5</div>
<div id="noconnect">NOT CONNECTED TO CLUCK API!</div>
<div class="button-grid" id="button-grid"></div>
<script src="/static/js/moses.js"></script>
<script src="./index.ts"></script>
</body>
</html>
Loading

0 comments on commit 9e73816

Please sign in to comment.