Add series management and ticketing features: Introduce series event functionality in event creation, enhance event display with series information, and implement ticketing options for public events. Update layouts and improve form handling for better user experience.
This commit is contained in:
parent
c3a29fa47c
commit
a88aa62198
24 changed files with 2897 additions and 44 deletions
|
|
@ -29,13 +29,32 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
await connectDB()
|
||||
|
||||
const newEvent = new Event({
|
||||
const eventData = {
|
||||
...body,
|
||||
createdBy: 'admin@ghostguild.org', // TODO: Use actual authenticated user
|
||||
startDate: new Date(body.startDate),
|
||||
endDate: new Date(body.endDate),
|
||||
registrationDeadline: body.registrationDeadline ? new Date(body.registrationDeadline) : null
|
||||
})
|
||||
}
|
||||
|
||||
// Handle ticket data
|
||||
if (body.tickets) {
|
||||
eventData.tickets = {
|
||||
enabled: body.tickets.enabled || false,
|
||||
public: {
|
||||
available: body.tickets.public?.available || false,
|
||||
name: body.tickets.public?.name || 'Public Ticket',
|
||||
description: body.tickets.public?.description || '',
|
||||
price: body.tickets.public?.price || 0,
|
||||
quantity: body.tickets.public?.quantity || null,
|
||||
sold: 0, // Initialize sold count
|
||||
earlyBirdPrice: body.tickets.public?.earlyBirdPrice || null,
|
||||
earlyBirdDeadline: body.tickets.public?.earlyBirdDeadline ? new Date(body.tickets.public.earlyBirdDeadline) : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newEvent = new Event(eventData)
|
||||
|
||||
const savedEvent = await newEvent.save()
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,23 @@ export default defineEventHandler(async (event) => {
|
|||
registrationDeadline: body.registrationDeadline ? new Date(body.registrationDeadline) : null,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
|
||||
// Handle ticket data
|
||||
if (body.tickets) {
|
||||
updateData.tickets = {
|
||||
enabled: body.tickets.enabled || false,
|
||||
public: {
|
||||
available: body.tickets.public?.available || false,
|
||||
name: body.tickets.public?.name || 'Public Ticket',
|
||||
description: body.tickets.public?.description || '',
|
||||
price: body.tickets.public?.price || 0,
|
||||
quantity: body.tickets.public?.quantity || null,
|
||||
sold: body.tickets.public?.sold || 0,
|
||||
earlyBirdPrice: body.tickets.public?.earlyBirdPrice || null,
|
||||
earlyBirdDeadline: body.tickets.public?.earlyBirdDeadline ? new Date(body.tickets.public.earlyBirdDeadline) : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatedEvent = await Event.findByIdAndUpdate(
|
||||
eventId,
|
||||
|
|
|
|||
60
server/api/admin/series.get.js
Normal file
60
server/api/admin/series.get.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import Series from '../../models/series.js'
|
||||
import Event from '../../models/event.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
// Fetch all series
|
||||
const series = await Series.find({ isActive: true })
|
||||
.sort({ createdAt: -1 })
|
||||
.lean()
|
||||
|
||||
// For each series, get event count and statistics
|
||||
const seriesWithStats = await Promise.all(
|
||||
series.map(async (s) => {
|
||||
const events = await Event.find({
|
||||
'series.id': s.id,
|
||||
'series.isSeriesEvent': true
|
||||
}).select('_id startDate endDate registrations').lean()
|
||||
|
||||
const now = new Date()
|
||||
const eventCount = events.length
|
||||
const completedEvents = events.filter(e => e.endDate < now).length
|
||||
const upcomingEvents = events.filter(e => e.startDate > now).length
|
||||
|
||||
const firstEventDate = events.length > 0 ?
|
||||
Math.min(...events.map(e => new Date(e.startDate))) : null
|
||||
const lastEventDate = events.length > 0 ?
|
||||
Math.max(...events.map(e => new Date(e.endDate))) : null
|
||||
|
||||
let status = 'upcoming'
|
||||
if (lastEventDate && lastEventDate < now) {
|
||||
status = 'completed'
|
||||
} else if (firstEventDate && firstEventDate <= now && lastEventDate && lastEventDate >= now) {
|
||||
status = 'active'
|
||||
}
|
||||
|
||||
return {
|
||||
...s,
|
||||
eventCount,
|
||||
completedEvents,
|
||||
upcomingEvents,
|
||||
startDate: firstEventDate,
|
||||
endDate: lastEventDate,
|
||||
status,
|
||||
totalRegistrations: events.reduce((sum, e) => sum + (e.registrations?.length || 0), 0)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return seriesWithStats
|
||||
} catch (error) {
|
||||
console.error('Error fetching series:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to fetch series'
|
||||
})
|
||||
}
|
||||
})
|
||||
49
server/api/admin/series.post.js
Normal file
49
server/api/admin/series.post.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import Series from '../../models/series.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validate required fields
|
||||
if (!body.id || !body.title || !body.description) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Series ID, title, and description are required'
|
||||
})
|
||||
}
|
||||
|
||||
// Create new series
|
||||
const newSeries = new Series({
|
||||
id: body.id,
|
||||
title: body.title,
|
||||
description: body.description,
|
||||
type: body.type || 'workshop_series',
|
||||
totalEvents: body.totalEvents || null,
|
||||
createdBy: 'admin' // TODO: Get from authentication
|
||||
})
|
||||
|
||||
await newSeries.save()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: newSeries
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating series:', error)
|
||||
|
||||
if (error.code === 11000) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'A series with this ID already exists'
|
||||
})
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: error.message || 'Failed to create series'
|
||||
})
|
||||
}
|
||||
})
|
||||
58
server/api/admin/series/[id].delete.js
Normal file
58
server/api/admin/series/[id].delete.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import Series from '../../../models/series.js'
|
||||
import Event from '../../../models/event.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
const id = getRouterParam(event, 'id')
|
||||
|
||||
if (!id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Series ID is required'
|
||||
})
|
||||
}
|
||||
|
||||
// Find the series
|
||||
const series = await Series.findOne({ id: id })
|
||||
|
||||
if (!series) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Series not found'
|
||||
})
|
||||
}
|
||||
|
||||
// Remove series relationship from all related events
|
||||
await Event.updateMany(
|
||||
{ 'series.id': id, 'series.isSeriesEvent': true },
|
||||
{
|
||||
$set: {
|
||||
'series.isSeriesEvent': false,
|
||||
'series.id': '',
|
||||
'series.title': '',
|
||||
'series.description': '',
|
||||
'series.type': 'workshop_series',
|
||||
'series.position': 1,
|
||||
'series.totalEvents': null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Delete the series
|
||||
await Series.deleteOne({ id: id })
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Series deleted and events converted to standalone events'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting series:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: error.message || 'Failed to delete series'
|
||||
})
|
||||
}
|
||||
})
|
||||
62
server/api/admin/series/[id].put.js
Normal file
62
server/api/admin/series/[id].put.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import Series from '../../../models/series.js'
|
||||
import Event from '../../../models/event.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
const id = getRouterParam(event, 'id')
|
||||
const body = await readBody(event)
|
||||
|
||||
if (!id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Series ID is required'
|
||||
})
|
||||
}
|
||||
|
||||
// Find and update the series
|
||||
const series = await Series.findOne({ id: id })
|
||||
|
||||
if (!series) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Series not found'
|
||||
})
|
||||
}
|
||||
|
||||
// Update series fields
|
||||
if (body.title !== undefined) series.title = body.title
|
||||
if (body.description !== undefined) series.description = body.description
|
||||
if (body.type !== undefined) series.type = body.type
|
||||
if (body.totalEvents !== undefined) series.totalEvents = body.totalEvents
|
||||
if (body.isActive !== undefined) series.isActive = body.isActive
|
||||
|
||||
await series.save()
|
||||
|
||||
// Also update all related events with the new series information
|
||||
await Event.updateMany(
|
||||
{ 'series.id': id, 'series.isSeriesEvent': true },
|
||||
{
|
||||
$set: {
|
||||
'series.title': series.title,
|
||||
'series.description': series.description,
|
||||
'series.type': series.type,
|
||||
'series.totalEvents': series.totalEvents
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: series
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating series:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: error.message || 'Failed to update series'
|
||||
})
|
||||
}
|
||||
})
|
||||
112
server/api/events/[id]/guest-register.post.js
Normal file
112
server/api/events/[id]/guest-register.post.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import Event from '../../../models/event.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await connectDB()
|
||||
const identifier = getRouterParam(event, 'id')
|
||||
const body = await readBody(event)
|
||||
|
||||
if (!identifier) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Event identifier is required'
|
||||
})
|
||||
}
|
||||
|
||||
// Validate required fields for guest registration
|
||||
if (!body.name || !body.email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Name and email are required'
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch the event
|
||||
let eventData
|
||||
if (mongoose.Types.ObjectId.isValid(identifier)) {
|
||||
eventData = await Event.findById(identifier)
|
||||
}
|
||||
if (!eventData) {
|
||||
eventData = await Event.findOne({ slug: identifier })
|
||||
}
|
||||
|
||||
if (!eventData) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if event allows public registration (not members-only)
|
||||
if (eventData.membersOnly) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'This event is for members only. Please become a member to register.'
|
||||
})
|
||||
}
|
||||
|
||||
// If event requires payment, reject guest registration
|
||||
if (eventData.pricing.paymentRequired && !eventData.pricing.isFree) {
|
||||
throw createError({
|
||||
statusCode: 402,
|
||||
statusMessage: 'This event requires payment. Please use the payment registration endpoint.'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if event is full
|
||||
if (eventData.maxAttendees && eventData.registrations.length >= eventData.maxAttendees) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Event is full'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if already registered
|
||||
const alreadyRegistered = eventData.registrations.some(
|
||||
reg => reg.email.toLowerCase() === body.email.toLowerCase()
|
||||
)
|
||||
|
||||
if (alreadyRegistered) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'You are already registered for this event'
|
||||
})
|
||||
}
|
||||
|
||||
// Add guest registration
|
||||
eventData.registrations.push({
|
||||
name: body.name,
|
||||
email: body.email.toLowerCase(),
|
||||
membershipLevel: 'guest',
|
||||
isMember: false,
|
||||
paymentStatus: 'not_required',
|
||||
amountPaid: 0,
|
||||
registeredAt: new Date()
|
||||
})
|
||||
|
||||
await eventData.save()
|
||||
|
||||
// TODO: Send confirmation email for guest registration
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Successfully registered as guest',
|
||||
registrationId: eventData.registrations[eventData.registrations.length - 1]._id,
|
||||
note: 'As a guest, you have access to this free public event. Consider becoming a member for access to all events!'
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error with guest registration:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to register as guest'
|
||||
})
|
||||
}
|
||||
})
|
||||
136
server/api/events/[id]/payment.post.js
Normal file
136
server/api/events/[id]/payment.post.js
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import Event from '../../../models/event.js'
|
||||
import Member from '../../../models/member.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
import { processHelcimPayment } from '../../../utils/helcim.js'
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await connectDB()
|
||||
const identifier = getRouterParam(event, 'id')
|
||||
const body = await readBody(event)
|
||||
|
||||
if (!identifier) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Event identifier is required'
|
||||
})
|
||||
}
|
||||
|
||||
// Validate required payment fields
|
||||
if (!body.name || !body.email || !body.paymentToken) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Name, email, and payment token are required'
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch the event
|
||||
let eventData
|
||||
if (mongoose.Types.ObjectId.isValid(identifier)) {
|
||||
eventData = await Event.findById(identifier)
|
||||
}
|
||||
if (!eventData) {
|
||||
eventData = await Event.findOne({ slug: identifier })
|
||||
}
|
||||
|
||||
if (!eventData) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if event requires payment
|
||||
if (eventData.pricing.isFree || !eventData.pricing.paymentRequired) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'This event does not require payment'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if user is already registered
|
||||
const existingRegistration = eventData.registrations.find(
|
||||
reg => reg.email.toLowerCase() === body.email.toLowerCase()
|
||||
)
|
||||
|
||||
if (existingRegistration) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'You are already registered for this event'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if user is a member (members get free access)
|
||||
const member = await Member.findOne({ email: body.email.toLowerCase() })
|
||||
|
||||
if (member) {
|
||||
// Members get free access - register directly without payment
|
||||
eventData.registrations.push({
|
||||
name: body.name,
|
||||
email: body.email.toLowerCase(),
|
||||
membershipLevel: `${member.circle}-${member.contributionTier}`,
|
||||
isMember: true,
|
||||
paymentStatus: 'not_required',
|
||||
amountPaid: 0
|
||||
})
|
||||
|
||||
await eventData.save()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Successfully registered as a member (no payment required)',
|
||||
registration: eventData.registrations[eventData.registrations.length - 1]
|
||||
}
|
||||
}
|
||||
|
||||
// Process payment for non-members
|
||||
const paymentResult = await processHelcimPayment({
|
||||
amount: eventData.pricing.publicPrice,
|
||||
paymentToken: body.paymentToken,
|
||||
customerData: {
|
||||
name: body.name,
|
||||
email: body.email
|
||||
}
|
||||
})
|
||||
|
||||
if (!paymentResult.success) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: paymentResult.message || 'Payment failed'
|
||||
})
|
||||
}
|
||||
|
||||
// Add registration with successful payment
|
||||
eventData.registrations.push({
|
||||
name: body.name,
|
||||
email: body.email.toLowerCase(),
|
||||
membershipLevel: 'non-member',
|
||||
isMember: false,
|
||||
paymentStatus: 'completed',
|
||||
paymentId: paymentResult.transactionId,
|
||||
amountPaid: eventData.pricing.publicPrice
|
||||
})
|
||||
|
||||
await eventData.save()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Payment successful and registered for event',
|
||||
paymentId: paymentResult.transactionId,
|
||||
registration: eventData.registrations[eventData.registrations.length - 1]
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing event payment:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to process payment and registration'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -65,27 +65,41 @@ export default defineEventHandler(async (event) => {
|
|||
})
|
||||
}
|
||||
|
||||
// Check member status if event is members-only
|
||||
if (eventData.membersOnly && body.membershipLevel === 'non-member') {
|
||||
// Check if email belongs to a member
|
||||
const member = await Member.findOne({ email: body.email.toLowerCase() })
|
||||
|
||||
if (!member) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'This event is for members only. Please become a member to register.'
|
||||
})
|
||||
}
|
||||
|
||||
// Update membership level from database
|
||||
body.membershipLevel = `${member.circle}-${member.contributionTier}`
|
||||
// Check member status and handle different registration scenarios
|
||||
const member = await Member.findOne({ email: body.email.toLowerCase() })
|
||||
|
||||
if (eventData.membersOnly && !member) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'This event is for members only. Please become a member to register.'
|
||||
})
|
||||
}
|
||||
|
||||
// If event requires payment and user is not a member, redirect to payment flow
|
||||
if (eventData.pricing.paymentRequired && !eventData.pricing.isFree && !member) {
|
||||
throw createError({
|
||||
statusCode: 402, // Payment Required
|
||||
statusMessage: 'This event requires payment. Please use the payment registration endpoint.'
|
||||
})
|
||||
}
|
||||
|
||||
// Set member status and membership level
|
||||
let isMember = false
|
||||
let membershipLevel = 'non-member'
|
||||
|
||||
if (member) {
|
||||
isMember = true
|
||||
membershipLevel = `${member.circle}-${member.contributionTier}`
|
||||
}
|
||||
|
||||
// Add registration
|
||||
eventData.registrations.push({
|
||||
name: body.name,
|
||||
email: body.email.toLowerCase(),
|
||||
membershipLevel: body.membershipLevel || 'non-member',
|
||||
membershipLevel,
|
||||
isMember,
|
||||
paymentStatus: 'not_required', // Free events or member registrations
|
||||
amountPaid: 0,
|
||||
dietary: body.dietary || false,
|
||||
registeredAt: new Date()
|
||||
})
|
||||
|
|
|
|||
78
server/api/series/[id].get.js
Normal file
78
server/api/series/[id].get.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import Event from '../../models/event.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
const id = getRouterParam(event, 'id')
|
||||
|
||||
if (!id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Series ID is required'
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch all events in this series
|
||||
const events = await Event.find({
|
||||
'series.id': id,
|
||||
'series.isSeriesEvent': true
|
||||
})
|
||||
.sort({ 'series.position': 1, startDate: 1 })
|
||||
.select('-registrations')
|
||||
.lean()
|
||||
|
||||
if (events.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Event series not found'
|
||||
})
|
||||
}
|
||||
|
||||
// Get series metadata from the first event
|
||||
const seriesInfo = events[0].series
|
||||
|
||||
// Calculate series statistics
|
||||
const now = new Date()
|
||||
const completedEvents = events.filter(e => e.endDate < now).length
|
||||
const upcomingEvents = events.filter(e => e.startDate > now).length
|
||||
const ongoingEvents = events.filter(e => e.startDate <= now && e.endDate >= now).length
|
||||
|
||||
const firstEventDate = events[0].startDate
|
||||
const lastEventDate = events[events.length - 1].endDate
|
||||
|
||||
// Return series with additional metadata
|
||||
return {
|
||||
id: id,
|
||||
title: seriesInfo.title,
|
||||
description: seriesInfo.description,
|
||||
type: seriesInfo.type,
|
||||
totalEvents: seriesInfo.totalEvents,
|
||||
startDate: firstEventDate,
|
||||
endDate: lastEventDate,
|
||||
events: events.map(e => ({
|
||||
...e,
|
||||
id: e._id.toString()
|
||||
})),
|
||||
statistics: {
|
||||
totalEvents: events.length,
|
||||
completedEvents,
|
||||
upcomingEvents,
|
||||
ongoingEvents,
|
||||
isOngoing: firstEventDate <= now && lastEventDate >= now,
|
||||
isUpcoming: firstEventDate > now,
|
||||
isCompleted: lastEventDate < now,
|
||||
totalRegistrations: events.reduce((sum, e) => sum + (e.registrations?.length || 0), 0)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching event series:', error)
|
||||
if (error.statusCode) throw error
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to fetch event series'
|
||||
})
|
||||
}
|
||||
})
|
||||
91
server/api/series/index.get.js
Normal file
91
server/api/series/index.get.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import Event from '../../models/event.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
const query = getQuery(event)
|
||||
|
||||
// Build filter for series events only
|
||||
const filter = {
|
||||
'series.isSeriesEvent': true,
|
||||
isVisible: query.includeHidden === 'true' ? { $exists: true } : true
|
||||
}
|
||||
|
||||
// Filter by series type
|
||||
if (query.seriesType) {
|
||||
filter['series.type'] = query.seriesType
|
||||
}
|
||||
|
||||
// Filter for upcoming series
|
||||
if (query.upcoming === 'true') {
|
||||
filter.startDate = { $gte: new Date() }
|
||||
}
|
||||
|
||||
// Fetch all series events and group them by series.id
|
||||
const events = await Event.find(filter)
|
||||
.sort({ 'series.id': 1, 'series.position': 1, startDate: 1 })
|
||||
.select('-registrations')
|
||||
.lean()
|
||||
|
||||
// Group events by series ID
|
||||
const seriesMap = new Map()
|
||||
|
||||
events.forEach(event => {
|
||||
const seriesId = event.series?.id
|
||||
if (!seriesId) return
|
||||
|
||||
if (!seriesMap.has(seriesId)) {
|
||||
seriesMap.set(seriesId, {
|
||||
id: seriesId,
|
||||
title: event.series.title,
|
||||
description: event.series.description,
|
||||
type: event.series.type,
|
||||
totalEvents: event.series.totalEvents,
|
||||
events: [],
|
||||
firstEventDate: event.startDate,
|
||||
lastEventDate: event.endDate
|
||||
})
|
||||
}
|
||||
|
||||
const series = seriesMap.get(seriesId)
|
||||
series.events.push({
|
||||
...event,
|
||||
id: event._id.toString()
|
||||
})
|
||||
|
||||
// Update date range
|
||||
if (event.startDate < series.firstEventDate) {
|
||||
series.firstEventDate = event.startDate
|
||||
}
|
||||
if (event.endDate > series.lastEventDate) {
|
||||
series.lastEventDate = event.endDate
|
||||
}
|
||||
})
|
||||
|
||||
// Convert to array and add computed fields
|
||||
const seriesArray = Array.from(seriesMap.values()).map(series => {
|
||||
const now = new Date()
|
||||
return {
|
||||
...series,
|
||||
eventCount: series.events.length,
|
||||
startDate: series.firstEventDate,
|
||||
endDate: series.lastEventDate,
|
||||
isOngoing: series.firstEventDate <= now && series.lastEventDate >= now,
|
||||
isUpcoming: series.firstEventDate > now,
|
||||
isCompleted: series.lastEventDate < now,
|
||||
status: series.lastEventDate < now ? 'completed' :
|
||||
series.firstEventDate <= now && series.lastEventDate >= now ? 'active' : 'upcoming'
|
||||
}
|
||||
})
|
||||
|
||||
return seriesArray
|
||||
} catch (error) {
|
||||
console.error('Error fetching event series:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to fetch event series'
|
||||
})
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue