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

@@ -33,7 +33,7 @@ export async function getMonthCalendarEvents(viewedMonth: Date): Promise<Calenda
// Base range: first and last day of the month
const firstOfMonth = new Date(year, month, 1);
const lastOfMonth = new Date(year, month + 1, 0);
const lastOfMonth = new Date(year, month + 1, 0);
// --- Apply 10 day padding ---
const start = new Date(firstOfMonth);
@@ -44,7 +44,7 @@ export async function getMonthCalendarEvents(viewedMonth: Date): Promise<Calenda
end.setHours(23, 59, 59, 999);
const from = start.toISOString();
const to = end.toISOString();
const to = end.toISOString();
const url = `${addr}/calendar?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`;
@@ -60,7 +60,7 @@ export async function getMonthCalendarEvents(viewedMonth: Date): Promise<Calenda
export async function getCalendarEvent(id: number): Promise<CalendarEvent> {
let res = await fetch(`${addr}/calendar/${id}`);
if(res.ok) {
if (res.ok) {
return await res.json();
} else {
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) {
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">
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 { computed } from 'vue';
import { computed, ref } from 'vue';
import ButtonGroup from '../ui/button-group/ButtonGroup.vue';
import Button from '../ui/button/Button.vue';
import { CalendarAttendance, setCalendarEventAttendance } from '@/api/calendar';
const props = defineProps<{
event: CalendarEvent | null
@@ -32,6 +33,11 @@ const whenText = computed(() => {
? `${startFmt.format(s)} ${endFmt.format(e)}`
: `${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>
<template>
@@ -48,23 +54,26 @@ const whenText = computed(() => {
</button>
</div>
<!-- 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>
<ButtonGroup>
<Button variant="outline">Going</Button>
<Button variant="outline">Maybe</Button>
<Button variant="outline">Declined</Button>
<Button variant="outline"
@click="setCalendarEventAttendance(activeEvent.id, CalendarAttendance.Attending)">Going</Button>
<Button variant="outline"
@click="setCalendarEventAttendance(activeEvent.id, CalendarAttendance.Maybe)">Maybe</Button>
<Button variant="outline"
@click="setCalendarEventAttendance(activeEvent.id, CalendarAttendance.NotAttending)">Declined</Button>
</ButtonGroup>
</section>
<!-- 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">
<Clock class="size-4 opacity-80" />
<span class="font-medium">{{ whenText }}</span>
</div>
</section>
<!-- 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">
<MapPin class="size-3.5 opacity-80" />
<span class="font-medium">{{ activeEvent.location || "Unknown" }}</span>
@@ -76,18 +85,38 @@ const whenText = computed(() => {
</span>
</section>
<!-- Description -->
<section class="space-y-2">
<section class="space-y-2 w-full">
<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">
{{ activeEvent.description }}
</p>
</section>
<!-- Attendance -->
<section class="space-y-2">
<section class="space-y-2 w-full">
<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">
{{ activeEvent.description }}
</p> -->
<div class="flex flex-col border bg-muted/50 rounded-lg min-h-24 my-2">
<div class="flex w-full pt-2 border-b *:w-full *:text-center *:pb-1 *:cursor-pointer">
<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>
</div>
<!-- Footer (optional actions) -->