Integrated attendance system

This commit is contained in:
2025-11-25 13:11:08 -05:00
parent 0a718d36c2
commit ca4f6a811f
5 changed files with 75 additions and 25 deletions

View File

@@ -1,6 +1,6 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { getEventAttendance, getEventDetails, getShortEventsInRange } from "../services/calendarService"; import { getEventAttendance, getEventDetails, getShortEventsInRange, setAttendanceStatus } from "../services/calendarService";
import { CalendarEvent } from "@app/shared/types/calendar"; import { CalendarAttendance, CalendarEvent } from "@app/shared/types/calendar";
const express = require('express'); const express = require('express');
const r = express.Router(); const r = express.Router();
@@ -35,6 +35,18 @@ r.get('/upcoming', async (req, res) => {
res.sendStatus(501); res.sendStatus(501);
}) })
r.post('/:id/attendance', async (req: Request, res: Response) => {
try {
let member = req.user.id;
let event = Number(req.params.id);
let state = req.query.state as CalendarAttendance;
setAttendanceStatus(member, event, state);
res.sendStatus(200);
} catch (error) {
console.error('Failed to set attendance:', error);
res.status(500).json(error);
}
})
//get event details //get event details
r.get('/:id', async (req: Request, res: Response) => { r.get('/:id', async (req: Request, res: Response) => {
try { try {
@@ -50,6 +62,7 @@ r.get('/:id', async (req: Request, res: Response) => {
} }
}) })
//post a new calendar event //post a new calendar event
r.post('/', async (req, res) => { r.post('/', async (req, res) => {

View File

@@ -1,7 +1,5 @@
import pool from '../db'; import pool from '../db';
import { CalendarEventShort, CalendarSignup, CalendarEvent } from "@app/shared/types/calendar" import { CalendarEventShort, CalendarSignup, CalendarEvent, CalendarAttendance } from "@app/shared/types/calendar"
export type Attendance = 'attending' | 'maybe' | 'not_attending';
export async function createEvent(eventObject: Omit<CalendarEvent, 'id' | 'created_at' | 'updated_at' | 'cancelled'>) { export async function createEvent(eventObject: Omit<CalendarEvent, 'id' | 'created_at' | 'updated_at' | 'cancelled'>) {
const sql = ` const sql = `
@@ -113,7 +111,7 @@ export async function getUpcomingEvents(date: Date, limit: number) {
} }
export async function setAttendanceStatus(memberID: number, eventID: number, status: Attendance) { export async function setAttendanceStatus(memberID: number, eventID: number, status: CalendarAttendance) {
const sql = ` const sql = `
INSERT INTO calendar_events_signups (member_id, event_id, status) INSERT INTO calendar_events_signups (member_id, event_id, status)
VALUES (?, ?, ?) VALUES (?, ?, ?)

View File

@@ -22,9 +22,10 @@ export enum CalendarAttendance {
} }
export interface CalendarSignup { export interface CalendarSignup {
memberID: number; member_id: number;
eventID: number; eventID: number;
state: CalendarAttendance; status: CalendarAttendance;
member_name?: string;
} }
export interface CalendarEventShort { export interface CalendarEventShort {

View File

@@ -60,7 +60,7 @@ export async function getMonthCalendarEvents(viewedMonth: Date): Promise<Calenda
export async function getCalendarEvent(id: number): Promise<CalendarEvent> { export async function getCalendarEvent(id: number): Promise<CalendarEvent> {
let res = await fetch(`${addr}/calendar/${id}`); let res = await fetch(`${addr}/calendar/${id}`);
if(res.ok) { if (res.ok) {
return await res.json(); return await res.json();
} else { } else {
throw new Error(`Failed to fetch event: ${res.status} ${res.statusText}`); throw new Error(`Failed to fetch event: ${res.status} ${res.statusText}`);
@@ -84,5 +84,14 @@ export async function adminCancelCalendarEvent(eventID: number) {
} }
export async function setCalendarEventAttendance(eventID: number, state: CalendarAttendance) { export async function setCalendarEventAttendance(eventID: number, state: CalendarAttendance) {
let res = await fetch(`${addr}/calendar/ ${eventID}/attendance?state=${state}`, {
method: "POST",
credentials: "include",
});
if (res.ok) {
return;
} else {
throw new Error(`Failed to set attendance: ${res.status} ${res.statusText}`);
}
} }

View File

@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CalendarEvent } from '@shared/types/calendar' import type { CalendarEvent, CalendarSignup } from '@shared/types/calendar'
import { Clock, MapPin, User, X } from 'lucide-vue-next'; import { Clock, MapPin, User, X } from 'lucide-vue-next';
import { computed } from 'vue'; import { computed, ref } from 'vue';
import ButtonGroup from '../ui/button-group/ButtonGroup.vue'; import ButtonGroup from '../ui/button-group/ButtonGroup.vue';
import Button from '../ui/button/Button.vue'; import Button from '../ui/button/Button.vue';
import { CalendarAttendance, setCalendarEventAttendance } from '@/api/calendar';
const props = defineProps<{ const props = defineProps<{
event: CalendarEvent | null event: CalendarEvent | null
@@ -32,6 +33,11 @@ const whenText = computed(() => {
? `${startFmt.format(s)} ${endFmt.format(e)}` ? `${startFmt.format(s)} ${endFmt.format(e)}`
: `${startFmt.format(s)}` : `${startFmt.format(s)}`
}) })
const attending = computed<CalendarSignup[]>(() => { return activeEvent.value.eventSignups.filter((s) => s.status == CalendarAttendance.Attending) })
const maybe = computed<CalendarSignup[]>(() => { return activeEvent.value.eventSignups.filter((s) => s.status == CalendarAttendance.Maybe) })
const declined = computed<CalendarSignup[]>(() => { return activeEvent.value.eventSignups.filter((s) => s.status == CalendarAttendance.NotAttending) })
const viewedState = ref<CalendarAttendance>(CalendarAttendance.Attending);
</script> </script>
<template> <template>
@@ -48,23 +54,26 @@ const whenText = computed(() => {
</button> </button>
</div> </div>
<!-- Body --> <!-- Body -->
<div class="flex-1 min-h-0 overflow-y-auto px-4 py-4 space-y-6"> <div class="flex-1 flex flex-col items-center min-h-0 overflow-y-auto px-4 py-4 space-y-6">
<section> <section>
<ButtonGroup> <ButtonGroup>
<Button variant="outline">Going</Button> <Button variant="outline"
<Button variant="outline">Maybe</Button> @click="setCalendarEventAttendance(activeEvent.id, CalendarAttendance.Attending)">Going</Button>
<Button variant="outline">Declined</Button> <Button variant="outline"
@click="setCalendarEventAttendance(activeEvent.id, CalendarAttendance.Maybe)">Maybe</Button>
<Button variant="outline"
@click="setCalendarEventAttendance(activeEvent.id, CalendarAttendance.NotAttending)">Declined</Button>
</ButtonGroup> </ButtonGroup>
</section> </section>
<!-- When --> <!-- When -->
<section v-if="whenText" class="space-y-2"> <section v-if="whenText" class="space-y-2 w-full">
<div class="inline-flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-sm"> <div class="inline-flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-sm">
<Clock class="size-4 opacity-80" /> <Clock class="size-4 opacity-80" />
<span class="font-medium">{{ whenText }}</span> <span class="font-medium">{{ whenText }}</span>
</div> </div>
</section> </section>
<!-- Quick meta chips --> <!-- Quick meta chips -->
<section class="flex flex-wrap gap-2"> <section class="flex flex-wrap gap-2 w-full">
<span class="inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs"> <span class="inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs">
<MapPin class="size-3.5 opacity-80" /> <MapPin class="size-3.5 opacity-80" />
<span class="font-medium">{{ activeEvent.location || "Unknown" }}</span> <span class="font-medium">{{ activeEvent.location || "Unknown" }}</span>
@@ -76,18 +85,38 @@ const whenText = computed(() => {
</span> </span>
</section> </section>
<!-- Description --> <!-- Description -->
<section class="space-y-2"> <section class="space-y-2 w-full">
<p class="text-lg font-semibold">Description</p> <p class="text-lg font-semibold">Description</p>
<p class="border bg-muted/50 px-3 py-2 rounded-lg min-h-24 my-2"> <p class="border bg-muted/50 px-3 py-2 rounded-lg min-h-24 my-2">
{{ activeEvent.description }} {{ activeEvent.description }}
</p> </p>
</section> </section>
<!-- Attendance --> <!-- Attendance -->
<section class="space-y-2"> <section class="space-y-2 w-full">
<p class="text-lg font-semibold">Attendance</p> <p class="text-lg font-semibold">Attendance</p>
<!-- <p class="border bg-muted/50 px-3 py-2 rounded-lg min-h-24 my-2"> <div class="flex flex-col border bg-muted/50 rounded-lg min-h-24 my-2">
{{ activeEvent.description }} <div class="flex w-full pt-2 border-b *:w-full *:text-center *:pb-1 *:cursor-pointer">
</p> --> <label :class="viewedState === CalendarAttendance.Attending ? 'border-b-3 border-foreground' : 'mb-[2px]'"
@click="viewedState = CalendarAttendance.Attending">Going {{ attending.length }}</label>
<label :class="viewedState === CalendarAttendance.Maybe ? 'border-b-3 border-foreground' : 'mb-[2px]'"
@click="viewedState = CalendarAttendance.Maybe">Maybe {{ maybe.length }}</label>
<label
:class="viewedState === CalendarAttendance.NotAttending ? 'border-b-3 border-foreground' : 'mb-[2px]'"
@click="viewedState = CalendarAttendance.NotAttending">Declined {{ declined.length
}}</label>
</div>
<div class="px-5 py-4 min-h-28">
<div v-if="viewedState === CalendarAttendance.Attending" v-for="person in attending">
<p>{{ person.member_name }}</p>
</div>
<div v-if="viewedState === CalendarAttendance.Maybe" v-for="person in maybe">
<p>{{ person.member_name }}</p>
</div>
<div v-if="viewedState === CalendarAttendance.NotAttending" v-for="person in declined">
<p>{{ person.member_name }}</p>
</div>
</div>
</div>
</section> </section>
</div> </div>
<!-- Footer (optional actions) --> <!-- Footer (optional actions) -->