diff --git a/api/src/index.js b/api/src/index.js index 59c1381..5f5cbac 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -46,6 +46,7 @@ const { status, memberStatus } = require('./routes/statuses') const authRouter = require('./routes/auth') const { roles, memberRoles } = require('./routes/roles'); const { courseRouter, eventRouter } = require('./routes/course'); +const { calendarRouter } = require('./routes/calendar') const morgan = require('morgan'); app.use('/application', applicationsRouter); @@ -59,6 +60,7 @@ app.use('/roles', roles) app.use('/memberRoles', memberRoles) app.use('/course', courseRouter) app.use('/courseEvent', eventRouter) +app.use('/calendar', calendarRouter) app.use('/', authRouter) app.get('/ping', (req, res) => { diff --git a/api/src/routes/calendar.ts b/api/src/routes/calendar.ts index 2154eec..9faa333 100644 --- a/api/src/routes/calendar.ts +++ b/api/src/routes/calendar.ts @@ -1,4 +1,6 @@ +import { Request, Response } from "express"; import { getEventAttendance, getEventDetails, getShortEventsInRange } from "../services/calendarService"; +import { CalendarEvent } from "@app/shared/types/calendar"; const express = require('express'); const r = express.Router(); @@ -9,16 +11,24 @@ function addMonths(date: Date, months: number): Date { return d } -//get calendar events paged +//get calendar events paged, requires a query string with from= and to= as mariadb ISO strings r.get('/', async (req, res) => { - const viewDate: Date = req.body.date; - //generate date range - const backDate: Date = addMonths(viewDate, -1); - const frontDate: Date = addMonths(viewDate, 2); + try { + const fromDate: string = req.query.from; + const toDate: string = req.query.to; - const events = getShortEventsInRange(backDate, frontDate); + if (fromDate === undefined || toDate === undefined) { + res.status(400).send("Missing required query parameters 'from' and 'to'"); + return; + } - res.status(200).json(events); + const events = await getShortEventsInRange(fromDate, toDate); + + res.status(200).json(events); + } catch (error) { + console.error('Error fetching calendar events:', error); + res.status(500).send('Error fetching calendar events'); + } }); r.get('/upcoming', async (req, res) => { @@ -26,19 +36,17 @@ r.get('/upcoming', async (req, res) => { }) //get event details -r.get('/:id', async (req, res) => { +r.get('/:id', async (req: Request, res: Response) => { try { - const eventID: number = req.params.id; + const eventID: number = Number(req.params.id); - let details = getEventDetails(eventID); - let attendance = await getEventAttendance(eventID); - - let out = { ...details, attendance } - console.log(out); - res.status(200).json(out); + let details: CalendarEvent = await getEventDetails(eventID); + details.eventSignups = await getEventAttendance(eventID); + console.log(details); + res.status(200).json(details); } catch (err) { console.error('Insert failed:', err); - res.status(500).json(err); + res.status(500).json(err); } }) @@ -47,4 +55,4 @@ r.post('/', async (req, res) => { }) -module.exports.calendar = r; \ No newline at end of file +module.exports.calendarRouter = r; \ No newline at end of file diff --git a/api/src/services/calendarService.d.ts b/api/src/services/calendarService.d.ts deleted file mode 100644 index 6273805..0000000 --- a/api/src/services/calendarService.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export declare function createEvent(eventObject: any): Promise; -export declare function updateEvent(eventObject: any): Promise; -export declare function cancelEvent(eventID: any): Promise; -export declare function getShortEventsInRange(startDate: any, endDate: any): Promise; -export declare function getEventDetailed(eventID: any): Promise; -//# sourceMappingURL=calendarService.d.ts.map \ No newline at end of file diff --git a/api/src/services/calendarService.d.ts.map b/api/src/services/calendarService.d.ts.map deleted file mode 100644 index 156c9d6..0000000 --- a/api/src/services/calendarService.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"calendarService.d.ts","sourceRoot":"","sources":["calendarService.ts"],"names":[],"mappings":"AAEA,wBAAsB,WAAW,CAAC,WAAW,KAAA,iBAE5C;AAED,wBAAsB,WAAW,CAAC,WAAW,KAAA,iBAE5C;AAED,wBAAsB,WAAW,CAAC,OAAO,KAAA,iBAExC;AAED,wBAAsB,qBAAqB,CAAC,SAAS,KAAA,EAAE,OAAO,KAAA,iBAE7D;AAED,wBAAsB,gBAAgB,CAAC,OAAO,KAAA,iBAE7C"} \ No newline at end of file diff --git a/api/src/services/calendarService.js b/api/src/services/calendarService.js deleted file mode 100644 index ee8e08e..0000000 --- a/api/src/services/calendarService.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createEvent = createEvent; -exports.updateEvent = updateEvent; -exports.cancelEvent = cancelEvent; -exports.getShortEventsInRange = getShortEventsInRange; -exports.getEventDetailed = getEventDetailed; -const pool = require('../db'); -async function createEvent(eventObject) { -} -async function updateEvent(eventObject) { -} -async function cancelEvent(eventID) { -} -async function getShortEventsInRange(startDate, endDate) { -} -async function getEventDetailed(eventID) { -} -//# sourceMappingURL=calendarService.js.map \ No newline at end of file diff --git a/api/src/services/calendarService.js.map b/api/src/services/calendarService.js.map deleted file mode 100644 index e8a02ee..0000000 --- a/api/src/services/calendarService.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"calendarService.js","sourceRoot":"","sources":["calendarService.ts"],"names":[],"mappings":";;AAEA,kCAEC;AAED,kCAEC;AAED,kCAEC;AAED,sDAEC;AAED,4CAEC;AApBD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;AAEtB,KAAK,UAAU,WAAW,CAAC,WAAW;AAE7C,CAAC;AAEM,KAAK,UAAU,WAAW,CAAC,WAAW;AAE7C,CAAC;AAEM,KAAK,UAAU,WAAW,CAAC,OAAO;AAEzC,CAAC;AAEM,KAAK,UAAU,qBAAqB,CAAC,SAAS,EAAE,OAAO;AAE9D,CAAC;AAEM,KAAK,UAAU,gBAAgB,CAAC,OAAO;AAE9C,CAAC"} \ No newline at end of file diff --git a/api/src/services/calendarService.ts b/api/src/services/calendarService.ts index 890889c..9c1a4c0 100644 --- a/api/src/services/calendarService.ts +++ b/api/src/services/calendarService.ts @@ -1,18 +1,5 @@ import pool from '../db'; - -export interface CalendarEvent { - id: number; - name: string; - start: Date; // DATETIME -> Date - end: Date; // DATETIME -> Date - location: string; - color: string; // 7 character hex string - description?: string | null; - creator?: number | null; // foreign key to members.id, nullable - cancelled: boolean; // TINYINT(1) -> boolean - created_at: Date; // TIMESTAMP -> Date - updated_at: Date; // TIMESTAMP -> Date -} +import { CalendarEventShort, CalendarSignup, CalendarEvent } from "@app/shared/types/calendar" export type Attendance = 'attending' | 'maybe' | 'not_attending'; @@ -29,7 +16,7 @@ export async function createEvent(eventObject: Omit { const sql = ` SELECT id, name, start, end, color FROM calendar_events WHERE start BETWEEN ? AND ? ORDER BY start ASC `; - return await pool.query(sql, [startDate, endDate]); + const res: CalendarEventShort[] = await pool.query(sql, [startDate, endDate]); + console.log(res); + return res; } -export async function getEventDetails(eventID: number) { +export async function getEventDetails(eventID: number): Promise { const sql = ` SELECT e.id, @@ -101,14 +90,14 @@ export async function getEventDetails(eventID: number) { e.cancelled, e.created_at, e.updated_at, - m.id AS creator_id, + e.creator AS creator_id, m.name AS creator_name FROM calendar_events e LEFT JOIN members m ON e.creator = m.id WHERE e.id = ? `; - - return await pool.query(sql, [eventID]) + let vals: CalendarEvent[] = await pool.query(sql, [eventID]); + return vals[0]; } export async function getUpcomingEvents(date: Date, limit: number) { @@ -135,7 +124,7 @@ export async function setAttendanceStatus(memberID: number, eventID: number, sta return { success: true } } -export async function getEventAttendance(eventID: number) { +export async function getEventAttendance(eventID: number): Promise { const sql = ` SELECT s.member_id, diff --git a/shared/types/calendar.ts b/shared/types/calendar.ts new file mode 100644 index 0000000..2aa38b2 --- /dev/null +++ b/shared/types/calendar.ts @@ -0,0 +1,36 @@ +export interface CalendarEvent { + id: number; + name: string; + start: Date; + end: Date; + location: string; + color: string; + description: string; + creator_id: number; + cancelled: boolean; + created_at: Date; + updated_at: Date; + + creator_name?: string | null; + eventSignups?: CalendarSignup[] | null; +} + +export enum CalendarAttendance { + Attending = "attending", + NotAttending = "not_attending", + Maybe = "maybe" +} + +export interface CalendarSignup { + memberID: number; + eventID: number; + state: CalendarAttendance; +} + +export interface CalendarEventShort { + id: number; + name: string; + start: Date; + end: Date; + color: string; +} \ No newline at end of file diff --git a/shared/utils/time.ts b/shared/utils/time.ts new file mode 100644 index 0000000..98d550b --- /dev/null +++ b/shared/utils/time.ts @@ -0,0 +1,11 @@ +export function toDateTime(date: Date): string { + // This produces a CST-local time because server runs in CST + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + const hour = date.getHours().toString().padStart(2, "0"); + const minute = date.getMinutes().toString().padStart(2, "0"); + const second = date.getSeconds().toString().padStart(2, "0"); + + return `${year}-${month}-${day} ${hour}:${minute}:${second}`; +} \ No newline at end of file diff --git a/ui/src/api/calendar.ts b/ui/src/api/calendar.ts index d4c01ac..533cfda 100644 --- a/ui/src/api/calendar.ts +++ b/ui/src/api/calendar.ts @@ -1,13 +1,13 @@ -export interface CalendarEvent { - name: string, - start: Date, - end: Date, - location: string, - color: string, - description: string, - creator: any | null, // user object - id: number | null -} +// export interface CalendarEvent { +// name: string, +// start: Date, +// end: Date, +// location: string, +// color: string, +// description: string, +// creator: any | null, // user object +// id: number | null +// } export enum CalendarAttendance { Attending = "attending", @@ -21,6 +21,52 @@ export interface CalendarSignup { state: CalendarAttendance } +import { CalendarEventShort, CalendarEvent } from "@shared/types/calendar"; + +//@ts-ignore +const addr = import.meta.env.VITE_APIHOST; + +export async function getMonthCalendarEvents(viewedMonth: Date): Promise { + + const year = viewedMonth.getFullYear(); + const month = viewedMonth.getMonth(); + + // Base range: first and last day of the month + const firstOfMonth = new Date(year, month, 1); + const lastOfMonth = new Date(year, month + 1, 0); + + // --- Apply 10 day padding --- + const start = new Date(firstOfMonth); + start.setDate(start.getDate() - 10); + + const end = new Date(lastOfMonth); + end.setDate(end.getDate() + 10); + end.setHours(23, 59, 59, 999); + + const from = start.toISOString(); + const to = end.toISOString(); + + const url = `${addr}/calendar?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`; + + const res = await fetch(url); + + if (!res.ok) { + throw new Error(`Failed to fetch events: ${res.status} ${res.statusText}`); + } + + return res.json(); +} + +export async function getCalendarEvent(id: number): Promise { + let res = await fetch(`${addr}/calendar/${id}`); + + if(res.ok) { + return await res.json(); + } else { + throw new Error(`Failed to fetch event: ${res.status} ${res.statusText}`); + } +} + export async function createCalendarEvent(eventData: CalendarEvent) { } @@ -34,7 +80,7 @@ export async function cancelCalendarEvent(eventID: number) { } export async function adminCancelCalendarEvent(eventID: number) { - + } export async function setCalendarEventAttendance(eventID: number, state: CalendarAttendance) { diff --git a/ui/src/pages/Calendar.vue b/ui/src/pages/Calendar.vue index e7b6c11..5f627ff 100644 --- a/ui/src/pages/Calendar.vue +++ b/ui/src/pages/Calendar.vue @@ -5,6 +5,9 @@ import dayGridPlugin from '@fullcalendar/daygrid' import interactionPlugin from '@fullcalendar/interaction' import { X, Clock, MapPin, User, ListTodo, ChevronLeft, ChevronRight, Plus } from 'lucide-vue-next' import CreateCalendarEvent from '@/components/calendar/CreateCalendarEvent.vue' +import { getCalendarEvent, getMonthCalendarEvents } from '@/api/calendar' +import { CalendarEvent, CalendarEventShort } from '@shared/types/calendar' +import { Calendar } from '@fullcalendar/core' const monthLabels = [ 'January', 'February', 'March', 'April', 'May', 'June', @@ -30,15 +33,18 @@ function buildFullDate(month: number, year: number): Date { return new Date(year, month, 1); //default to first of month } - -watch([selectedMonth, selectedYear], () => { +watch([selectedMonth, selectedYear], async () => { console.log('Selected date changed:', selectedMonth.value, selectedYear.value) - + let monthEvents = await getMonthCalendarEvents(buildFullDate(selectedMonth.value, selectedYear.value)); + events.value = monthEvents.map(toCalEvent); + console.log(events.value); }) -onMounted(() => { - // fetchEventsFor(selectedMonth.value, selectedYear.value) +onMounted(async () => { + let monthEvents = await getMonthCalendarEvents(buildFullDate(selectedMonth.value, selectedYear.value)); + events.value = monthEvents.map(toCalEvent); + console.log(events.value); }) @@ -54,28 +60,41 @@ function goToSelectedDate() { type CalEvent = { id: string title: string - start: string - end?: string + start: Date + end?: Date extendedProps?: Record } -const events = ref([ - { id: '1', title: 'Squad Training', start: '2025-10-08T19:00:00', extendedProps: { trainer: 'Alex', location: 'Range A', color: '#88C4FF' } }, - { id: '2', title: 'Ops Briefing', start: '2025-10-09T20:30:00', extendedProps: { owner: 'CO', agenda: ['Weather', 'Route', 'Risks'], color: '#dba42c' } }, -]) +function toCalEvent(e: CalendarEventShort): CalEvent { + return { + id: e.id.toString(), + title: e.name, + start: e.start, + end: e.end, + extendedProps: { + color: e.color + } + } +} + + +const events = ref([]) const panelOpen = ref(false) -const activeEvent = ref(null) +const activeEvent = ref(null) const calendarRef = ref | null>(null) -function onEventClick(arg: any) { - activeEvent.value = { - id: arg.event.id, - title: arg.event.title, - start: arg.event.startStr, - end: arg.event.endStr, - extendedProps: arg.event.extendedProps - } +async function onEventClick(arg: any) { + const targetEvent = arg.event.id; + activeEvent.value = await getCalendarEvent(targetEvent); + console.log(activeEvent.value); + // activeEvent.value = { + // id: arg.event.id, + // title: arg.event.title, + // start: arg.event.startStr, + // end: arg.event.endStr, + // extendedProps: arg.event.extendedProps + // } panelOpen.value = true } @@ -123,6 +142,7 @@ const calendarOptions = ref({ // custom renderer -> one-line pill eventContent(arg) { + console.log const ext = arg.event.extendedProps || {} const c = ext.color || arg.backgroundColor || arg.borderColor || '' @@ -183,156 +203,143 @@ onMounted(() => { onDatesSet() }) -const ext = computed(() => activeEvent.value?.extendedProps ?? {}) +// const ext = computed(() => activeEvent.value?.extendedProps ?? {})