Integrated attendance system
This commit is contained in:
@@ -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) => {
|
||||||
|
|
||||||
|
|||||||
@@ -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 (?, ?, ?)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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) -->
|
||||||
|
|||||||
Reference in New Issue
Block a user