Hooked up calendar viewing to API, still needs a lot more polish
This commit is contained in:
@@ -46,6 +46,7 @@ const { status, memberStatus } = require('./routes/statuses')
|
|||||||
const authRouter = require('./routes/auth')
|
const authRouter = require('./routes/auth')
|
||||||
const { roles, memberRoles } = require('./routes/roles');
|
const { roles, memberRoles } = require('./routes/roles');
|
||||||
const { courseRouter, eventRouter } = require('./routes/course');
|
const { courseRouter, eventRouter } = require('./routes/course');
|
||||||
|
const { calendarRouter } = require('./routes/calendar')
|
||||||
const morgan = require('morgan');
|
const morgan = require('morgan');
|
||||||
|
|
||||||
app.use('/application', applicationsRouter);
|
app.use('/application', applicationsRouter);
|
||||||
@@ -59,6 +60,7 @@ app.use('/roles', roles)
|
|||||||
app.use('/memberRoles', memberRoles)
|
app.use('/memberRoles', memberRoles)
|
||||||
app.use('/course', courseRouter)
|
app.use('/course', courseRouter)
|
||||||
app.use('/courseEvent', eventRouter)
|
app.use('/courseEvent', eventRouter)
|
||||||
|
app.use('/calendar', calendarRouter)
|
||||||
app.use('/', authRouter)
|
app.use('/', authRouter)
|
||||||
|
|
||||||
app.get('/ping', (req, res) => {
|
app.get('/ping', (req, res) => {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
import { getEventAttendance, getEventDetails, getShortEventsInRange } from "../services/calendarService";
|
import { getEventAttendance, getEventDetails, getShortEventsInRange } from "../services/calendarService";
|
||||||
|
import { CalendarEvent } from "@app/shared/types/calendar";
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const r = express.Router();
|
const r = express.Router();
|
||||||
@@ -9,16 +11,24 @@ function addMonths(date: Date, months: number): Date {
|
|||||||
return d
|
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) => {
|
r.get('/', async (req, res) => {
|
||||||
const viewDate: Date = req.body.date;
|
try {
|
||||||
//generate date range
|
const fromDate: string = req.query.from;
|
||||||
const backDate: Date = addMonths(viewDate, -1);
|
const toDate: string = req.query.to;
|
||||||
const frontDate: Date = addMonths(viewDate, 2);
|
|
||||||
|
|
||||||
const events = getShortEventsInRange(backDate, frontDate);
|
if (fromDate === undefined || toDate === undefined) {
|
||||||
|
res.status(400).send("Missing required query parameters 'from' and 'to'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await getShortEventsInRange(fromDate, toDate);
|
||||||
|
|
||||||
res.status(200).json(events);
|
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) => {
|
r.get('/upcoming', async (req, res) => {
|
||||||
@@ -26,16 +36,14 @@ r.get('/upcoming', async (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
//get event details
|
//get event details
|
||||||
r.get('/:id', async (req, res) => {
|
r.get('/:id', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const eventID: number = req.params.id;
|
const eventID: number = Number(req.params.id);
|
||||||
|
|
||||||
let details = getEventDetails(eventID);
|
let details: CalendarEvent = await getEventDetails(eventID);
|
||||||
let attendance = await getEventAttendance(eventID);
|
details.eventSignups = await getEventAttendance(eventID);
|
||||||
|
console.log(details);
|
||||||
let out = { ...details, attendance }
|
res.status(200).json(details);
|
||||||
console.log(out);
|
|
||||||
res.status(200).json(out);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Insert failed:', 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;
|
module.exports.calendarRouter = r;
|
||||||
6
api/src/services/calendarService.d.ts
vendored
6
api/src/services/calendarService.d.ts
vendored
@@ -1,6 +0,0 @@
|
|||||||
export declare function createEvent(eventObject: any): Promise<void>;
|
|
||||||
export declare function updateEvent(eventObject: any): Promise<void>;
|
|
||||||
export declare function cancelEvent(eventID: any): Promise<void>;
|
|
||||||
export declare function getShortEventsInRange(startDate: any, endDate: any): Promise<void>;
|
|
||||||
export declare function getEventDetailed(eventID: any): Promise<void>;
|
|
||||||
//# sourceMappingURL=calendarService.d.ts.map
|
|
||||||
@@ -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"}
|
|
||||||
@@ -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
|
|
||||||
@@ -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"}
|
|
||||||
@@ -1,18 +1,5 @@
|
|||||||
import pool from '../db';
|
import pool from '../db';
|
||||||
|
import { CalendarEventShort, CalendarSignup, CalendarEvent } from "@app/shared/types/calendar"
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Attendance = 'attending' | 'maybe' | 'not_attending';
|
export type Attendance = 'attending' | 'maybe' | 'not_attending';
|
||||||
|
|
||||||
@@ -29,7 +16,7 @@ export async function createEvent(eventObject: Omit<CalendarEvent, 'id' | 'creat
|
|||||||
eventObject.location,
|
eventObject.location,
|
||||||
eventObject.color,
|
eventObject.color,
|
||||||
eventObject.description ?? null,
|
eventObject.description ?? null,
|
||||||
eventObject.creator,
|
eventObject.creator_id,
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = await pool.query(sql, params);
|
const result = await pool.query(sql, params);
|
||||||
@@ -78,17 +65,19 @@ export async function cancelEvent(eventID: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getShortEventsInRange(startDate: Date, endDate: Date) {
|
export async function getShortEventsInRange(startDate: string, endDate: string): Promise<CalendarEventShort[]> {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT id, name, start, end, color
|
SELECT id, name, start, end, color
|
||||||
FROM calendar_events
|
FROM calendar_events
|
||||||
WHERE start BETWEEN ? AND ?
|
WHERE start BETWEEN ? AND ?
|
||||||
ORDER BY start ASC
|
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<CalendarEvent> {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
e.id,
|
e.id,
|
||||||
@@ -101,14 +90,14 @@ export async function getEventDetails(eventID: number) {
|
|||||||
e.cancelled,
|
e.cancelled,
|
||||||
e.created_at,
|
e.created_at,
|
||||||
e.updated_at,
|
e.updated_at,
|
||||||
m.id AS creator_id,
|
e.creator AS creator_id,
|
||||||
m.name AS creator_name
|
m.name AS creator_name
|
||||||
FROM calendar_events e
|
FROM calendar_events e
|
||||||
LEFT JOIN members m ON e.creator = m.id
|
LEFT JOIN members m ON e.creator = m.id
|
||||||
WHERE e.id = ?
|
WHERE e.id = ?
|
||||||
`;
|
`;
|
||||||
|
let vals: CalendarEvent[] = await pool.query(sql, [eventID]);
|
||||||
return await pool.query(sql, [eventID])
|
return vals[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUpcomingEvents(date: Date, limit: number) {
|
export async function getUpcomingEvents(date: Date, limit: number) {
|
||||||
@@ -135,7 +124,7 @@ export async function setAttendanceStatus(memberID: number, eventID: number, sta
|
|||||||
return { success: true }
|
return { success: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEventAttendance(eventID: number) {
|
export async function getEventAttendance(eventID: number): Promise<CalendarSignup[]> {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
s.member_id,
|
s.member_id,
|
||||||
|
|||||||
36
shared/types/calendar.ts
Normal file
36
shared/types/calendar.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
11
shared/utils/time.ts
Normal file
11
shared/utils/time.ts
Normal file
@@ -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}`;
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
export interface CalendarEvent {
|
// export interface CalendarEvent {
|
||||||
name: string,
|
// name: string,
|
||||||
start: Date,
|
// start: Date,
|
||||||
end: Date,
|
// end: Date,
|
||||||
location: string,
|
// location: string,
|
||||||
color: string,
|
// color: string,
|
||||||
description: string,
|
// description: string,
|
||||||
creator: any | null, // user object
|
// creator: any | null, // user object
|
||||||
id: number | null
|
// id: number | null
|
||||||
}
|
// }
|
||||||
|
|
||||||
export enum CalendarAttendance {
|
export enum CalendarAttendance {
|
||||||
Attending = "attending",
|
Attending = "attending",
|
||||||
@@ -21,6 +21,52 @@ export interface CalendarSignup {
|
|||||||
state: CalendarAttendance
|
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<CalendarEventShort[]> {
|
||||||
|
|
||||||
|
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<CalendarEvent> {
|
||||||
|
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) {
|
export async function createCalendarEvent(eventData: CalendarEvent) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import dayGridPlugin from '@fullcalendar/daygrid'
|
|||||||
import interactionPlugin from '@fullcalendar/interaction'
|
import interactionPlugin from '@fullcalendar/interaction'
|
||||||
import { X, Clock, MapPin, User, ListTodo, ChevronLeft, ChevronRight, Plus } from 'lucide-vue-next'
|
import { X, Clock, MapPin, User, ListTodo, ChevronLeft, ChevronRight, Plus } from 'lucide-vue-next'
|
||||||
import CreateCalendarEvent from '@/components/calendar/CreateCalendarEvent.vue'
|
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 = [
|
const monthLabels = [
|
||||||
'January', 'February', 'March', 'April', 'May', 'June',
|
'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
|
return new Date(year, month, 1); //default to first of month
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch([selectedMonth, selectedYear], async () => {
|
||||||
watch([selectedMonth, selectedYear], () => {
|
|
||||||
console.log('Selected date changed:', selectedMonth.value, selectedYear.value)
|
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(() => {
|
onMounted(async () => {
|
||||||
// fetchEventsFor(selectedMonth.value, selectedYear.value)
|
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 = {
|
type CalEvent = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
start: string
|
start: Date
|
||||||
end?: string
|
end?: Date
|
||||||
extendedProps?: Record<string, any>
|
extendedProps?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = ref<CalEvent[]>([
|
function toCalEvent(e: CalendarEventShort): CalEvent {
|
||||||
{ id: '1', title: 'Squad Training', start: '2025-10-08T19:00:00', extendedProps: { trainer: 'Alex', location: 'Range A', color: '#88C4FF' } },
|
return {
|
||||||
{ id: '2', title: 'Ops Briefing', start: '2025-10-09T20:30:00', extendedProps: { owner: 'CO', agenda: ['Weather', 'Route', 'Risks'], color: '#dba42c' } },
|
id: e.id.toString(),
|
||||||
])
|
title: e.name,
|
||||||
|
start: e.start,
|
||||||
|
end: e.end,
|
||||||
|
extendedProps: {
|
||||||
|
color: e.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const events = ref<CalEvent[]>([])
|
||||||
|
|
||||||
const panelOpen = ref(false)
|
const panelOpen = ref(false)
|
||||||
const activeEvent = ref<CalEvent | null>(null)
|
const activeEvent = ref<CalendarEvent | null>(null)
|
||||||
const calendarRef = ref<InstanceType<typeof FullCalendar> | null>(null)
|
const calendarRef = ref<InstanceType<typeof FullCalendar> | null>(null)
|
||||||
|
|
||||||
function onEventClick(arg: any) {
|
async function onEventClick(arg: any) {
|
||||||
activeEvent.value = {
|
const targetEvent = arg.event.id;
|
||||||
id: arg.event.id,
|
activeEvent.value = await getCalendarEvent(targetEvent);
|
||||||
title: arg.event.title,
|
console.log(activeEvent.value);
|
||||||
start: arg.event.startStr,
|
// activeEvent.value = {
|
||||||
end: arg.event.endStr,
|
// id: arg.event.id,
|
||||||
extendedProps: arg.event.extendedProps
|
// title: arg.event.title,
|
||||||
}
|
// start: arg.event.startStr,
|
||||||
|
// end: arg.event.endStr,
|
||||||
|
// extendedProps: arg.event.extendedProps
|
||||||
|
// }
|
||||||
panelOpen.value = true
|
panelOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +142,7 @@ const calendarOptions = ref({
|
|||||||
|
|
||||||
// custom renderer -> one-line pill
|
// custom renderer -> one-line pill
|
||||||
eventContent(arg) {
|
eventContent(arg) {
|
||||||
|
console.log
|
||||||
const ext = arg.event.extendedProps || {}
|
const ext = arg.event.extendedProps || {}
|
||||||
const c = ext.color || arg.backgroundColor || arg.borderColor || ''
|
const c = ext.color || arg.backgroundColor || arg.borderColor || ''
|
||||||
|
|
||||||
@@ -183,12 +203,12 @@ onMounted(() => {
|
|||||||
onDatesSet()
|
onDatesSet()
|
||||||
})
|
})
|
||||||
|
|
||||||
const ext = computed(() => activeEvent.value?.extendedProps ?? {})
|
// const ext = computed(() => activeEvent.value?.extendedProps ?? {})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div>
|
||||||
<CreateCalendarEvent ref="dialogRef"></CreateCalendarEvent>
|
<CreateCalendarEvent ref="dialogRef"></CreateCalendarEvent>
|
||||||
|
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-1 min-h-0 mt-5">
|
<div class="flex-1 min-h-0 mt-5">
|
||||||
<div class="h-[80vh] min-h-0">
|
<div class="h-[80vh] min-h-0">
|
||||||
@@ -199,15 +219,14 @@ const ext = computed(() => activeEvent.value?.extendedProps ?? {})
|
|||||||
<!-- <h2 class="text-xl font-semibold tracking-tight">
|
<!-- <h2 class="text-xl font-semibold tracking-tight">
|
||||||
{{ monthLabels[selectedMonth] }} {{ selectedYear }}
|
{{ monthLabels[selectedMonth] }} {{ selectedYear }}
|
||||||
</h2> -->
|
</h2> -->
|
||||||
|
|
||||||
<!-- Month dropdown -->
|
<!-- Month dropdown -->
|
||||||
<select v-model.number="selectedMonth" @change="goToSelectedDate"
|
<select v-model.number="selectedMonth" @change="goToSelectedDate"
|
||||||
class="ml-2 rounded-md border bg-transparent px-2 py-1 text-sm" aria-label="Select month">
|
class="ml-2 rounded-md border bg-transparent px-2 py-1 text-sm"
|
||||||
|
aria-label="Select month">
|
||||||
<option v-for="(m, i) in monthLabels" :key="m" :value="i" class="bg-card">
|
<option v-for="(m, i) in monthLabels" :key="m" :value="i" class="bg-card">
|
||||||
{{ m }}
|
{{ m }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- Year dropdown -->
|
<!-- Year dropdown -->
|
||||||
<select v-model.number="selectedYear" @change="goToSelectedDate"
|
<select v-model.number="selectedYear" @change="goToSelectedDate"
|
||||||
class="rounded-md border bg-transparent px-2 py-1 text-sm" aria-label="Select year">
|
class="rounded-md border bg-transparent px-2 py-1 text-sm" aria-label="Select year">
|
||||||
@@ -216,7 +235,6 @@ const ext = computed(() => activeEvent.value?.extendedProps ?? {})
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: nav + today + create -->
|
<!-- Right: nav + today + create -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -240,18 +258,16 @@ const ext = computed(() => activeEvent.value?.extendedProps ?? {})
|
|||||||
Create
|
Create
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<FullCalendar ref="calendarRef" :options="calendarOptions" />
|
<FullCalendar ref="calendarRef" :options="calendarOptions" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<aside v-if="panelOpen && activeEvent" class="3xl:w-lg 2xl:w-md border-l bg-card text-foreground flex flex-col"
|
||||||
<aside v-if="panelOpen" class="3xl:w-lg 2xl:w-md border-l bg-card text-foreground flex flex-col"
|
|
||||||
:style="{ height: 'calc(100vh - 61px)', position: 'sticky', top: '64px' }">
|
:style="{ height: 'calc(100vh - 61px)', position: 'sticky', top: '64px' }">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between gap-3 border-b px-4 py-3">
|
<div class="flex items-center justify-between gap-3 border-b px-4 py-3">
|
||||||
<h2 class="text-lg font-semibold line-clamp-2">
|
<h2 class="text-lg font-semibold line-clamp-2">
|
||||||
{{ activeEvent?.title || 'Event' }}
|
{{ activeEvent?.name || 'Event' }}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
class="inline-flex size-8 items-center justify-center rounded-md border hover:bg-muted/40 transition"
|
class="inline-flex size-8 items-center justify-center rounded-md border hover:bg-muted/40 transition"
|
||||||
@@ -259,7 +275,6 @@ const ext = computed(() => activeEvent.value?.extendedProps ?? {})
|
|||||||
<X class="size-4" />
|
<X class="size-4" />
|
||||||
</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 min-h-0 overflow-y-auto px-4 py-4 space-y-6">
|
||||||
<!-- When -->
|
<!-- When -->
|
||||||
@@ -269,27 +284,21 @@ const ext = computed(() => activeEvent.value?.extendedProps ?? {})
|
|||||||
<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">
|
||||||
<span v-if="ext.location"
|
<span
|
||||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs">
|
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">{{ ext.location }}</span>
|
<span class="font-medium">{{ activeEvent.location || "Unknown" }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="ext.owner" class="inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs">
|
<span
|
||||||
<User class="size-3.5 opacity-80" />
|
|
||||||
<span class="font-medium">Owner: {{ ext.owner }}</span>
|
|
||||||
</span>
|
|
||||||
<span v-if="ext.trainer"
|
|
||||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs">
|
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs">
|
||||||
<User class="size-3.5 opacity-80" />
|
<User class="size-3.5 opacity-80" />
|
||||||
<span class="font-medium">Trainer: {{ ext.trainer }}</span>
|
<span class="font-medium">Owner: {{ activeEvent.creator_name || "Unknown User" }}</span>
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Agenda (special-cased array) -->
|
<!-- Agenda (special-cased array) -->
|
||||||
<section v-if="Array.isArray(ext.agenda) && ext.agenda.length" class="space-y-2">
|
<!-- <section v-if="Array.isArray(ext.agenda) && ext.agenda.length" class="space-y-2">
|
||||||
<div class="flex items-center gap-2 text-sm font-medium">
|
<div class="flex items-center gap-2 text-sm font-medium">
|
||||||
<ListTodo class="size-4 opacity-80" />
|
<ListTodo class="size-4 opacity-80" />
|
||||||
Agenda
|
Agenda
|
||||||
@@ -300,10 +309,9 @@ const ext = computed(() => activeEvent.value?.extendedProps ?? {})
|
|||||||
<span>{{ item }}</span>
|
<span>{{ item }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section> -->
|
||||||
|
|
||||||
<!-- Generic details (extendedProps minus the ones above) -->
|
<!-- Generic details (extendedProps minus the ones above) -->
|
||||||
<section v-if="ext && Object.keys(ext).length" class="space-y-3">
|
<!-- <section v-if="ext && Object.keys(ext).length" class="space-y-3">
|
||||||
<div class="text-sm font-medium opacity-80">Details</div>
|
<div class="text-sm font-medium opacity-80">Details</div>
|
||||||
<dl class="grid grid-cols-1 gap-y-3">
|
<dl class="grid grid-cols-1 gap-y-3">
|
||||||
<template v-for="(val, key) in ext" :key="key">
|
<template v-for="(val, key) in ext" :key="key">
|
||||||
@@ -318,9 +326,8 @@ const ext = computed(() => activeEvent.value?.extendedProps ?? {})
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer (optional actions) -->
|
<!-- Footer (optional actions) -->
|
||||||
<div class="border-t px-4 py-3 flex items-center justify-end gap-2">
|
<div class="border-t px-4 py-3 flex items-center justify-end gap-2">
|
||||||
<button class="rounded-md border px-3 py-1.5 text-sm hover:bg-muted/40 transition">
|
<button class="rounded-md border px-3 py-1.5 text-sm hover:bg-muted/40 transition">
|
||||||
@@ -332,7 +339,7 @@ const ext = computed(() => activeEvent.value?.extendedProps ?? {})
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user