20-calendar-system #37
@@ -1,6 +1,6 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getEventAttendance, getEventDetails, getShortEventsInRange } from "../services/calendarService";
|
||||
import { CalendarEvent } from "@app/shared/types/calendar";
|
||||
import { getEventAttendance, getEventDetails, getShortEventsInRange, setAttendanceStatus } from "../services/calendarService";
|
||||
import { CalendarAttendance, CalendarEvent } from "@app/shared/types/calendar";
|
||||
|
||||
const express = require('express');
|
||||
const r = express.Router();
|
||||
@@ -35,6 +35,18 @@ r.get('/upcoming', async (req, res) => {
|
||||
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
|
||||
r.get('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
@@ -50,6 +62,7 @@ r.get('/:id', async (req: Request, res: Response) => {
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
//post a new calendar event
|
||||
r.post('/', async (req, res) => {
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import pool from '../db';
|
||||
import { CalendarEventShort, CalendarSignup, CalendarEvent } from "@app/shared/types/calendar"
|
||||
|
||||
export type Attendance = 'attending' | 'maybe' | 'not_attending';
|
||||
import { CalendarEventShort, CalendarSignup, CalendarEvent, CalendarAttendance } from "@app/shared/types/calendar"
|
||||
|
||||
export async function createEvent(eventObject: Omit<CalendarEvent, 'id' | 'created_at' | 'updated_at' | 'cancelled'>) {
|
||||
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 = `
|
||||
INSERT INTO calendar_events_signups (member_id, event_id, status)
|
||||
VALUES (?, ?, ?)
|
||||
|
||||
@@ -22,9 +22,10 @@ export enum CalendarAttendance {
|
||||
}
|
||||
|
||||
export interface CalendarSignup {
|
||||
memberID: number;
|
||||
member_id: number;
|
||||
eventID: number;
|
||||
state: CalendarAttendance;
|
||||
status: CalendarAttendance;
|
||||
member_name?: string;
|
||||
}
|
||||
|
||||
export interface CalendarEventShort {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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) -->
|
||||
|
||||
Reference in New Issue
Block a user