Add event management system with calendar and CRUD operations
Some checks failed
Build And Push Image / docker (push) Failing after 2m37s
Some checks failed
Build And Push Image / docker (push) Failing after 2m37s
- Add EventCalendar component with FullCalendar integration - Create event CRUD dialogs and upcoming event banner - Implement server-side events API and database utilities - Add events dashboard page and navigation - Improve dues calculation with better overdue day logic - Install FullCalendar and date-fns dependencies
This commit is contained in:
95
server/api/events/[id]/attendees.patch.ts
Normal file
95
server/api/events/[id]/attendees.patch.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
// server/api/events/[id]/attendees.patch.ts
|
||||
import { createNocoDBEventsClient } from '~/server/utils/nocodb-events';
|
||||
import type { EventAttendanceRequest } from '~/utils/types';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const eventId = getRouterParam(event, 'id');
|
||||
const body = await readBody(event) as EventAttendanceRequest;
|
||||
|
||||
if (!eventId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Event ID is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Get user session
|
||||
const session = await getUserSession(event);
|
||||
if (!session || !session.user) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Authentication required'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user has permission to mark attendance (board or admin only)
|
||||
if (session.user.tier !== 'board' && session.user.tier !== 'admin') {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Only board members and administrators can mark attendance'
|
||||
});
|
||||
}
|
||||
|
||||
const eventsClient = createNocoDBEventsClient();
|
||||
|
||||
// Verify event exists
|
||||
const eventDetails = await eventsClient.findOne(eventId);
|
||||
if (!eventDetails) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Event not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Find the user's RSVP record
|
||||
const userRSVP = await eventsClient.findUserRSVP(eventId, body.member_id);
|
||||
if (!userRSVP) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'RSVP record not found for this member'
|
||||
});
|
||||
}
|
||||
|
||||
// Update attendance status
|
||||
const updatedRSVP = await eventsClient.updateRSVP(userRSVP.id, {
|
||||
attended: body.attended ? 'true' : 'false',
|
||||
updated_at: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedRSVP,
|
||||
message: `Attendance ${body.attended ? 'marked' : 'unmarked'} successfully`
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating attendance:', error);
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to update attendance'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function
|
||||
async function getUserSession(event: any) {
|
||||
try {
|
||||
const sessionCookie = getCookie(event, 'session') || getHeader(event, 'authorization');
|
||||
if (!sessionCookie) return null;
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: 'user-id',
|
||||
tier: 'board' // Replace with actual session logic
|
||||
}
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
199
server/api/events/[id]/rsvp.post.ts
Normal file
199
server/api/events/[id]/rsvp.post.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
// server/api/events/[id]/rsvp.post.ts
|
||||
import { createNocoDBEventsClient } from '~/server/utils/nocodb-events';
|
||||
import { createNocoDBClient } from '~/server/utils/nocodb';
|
||||
import type { EventRSVPRequest } from '~/utils/types';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const eventId = getRouterParam(event, 'id');
|
||||
const body = await readBody(event) as EventRSVPRequest;
|
||||
|
||||
if (!eventId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Event ID is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Get user session
|
||||
const session = await getUserSession(event);
|
||||
if (!session || !session.user) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Authentication required'
|
||||
});
|
||||
}
|
||||
|
||||
const eventsClient = createNocoDBEventsClient();
|
||||
const membersClient = createNocoDBClient();
|
||||
|
||||
// Get the event details
|
||||
const eventDetails = await eventsClient.findOne(eventId);
|
||||
if (!eventDetails) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Event not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if event is active
|
||||
if (eventDetails.status !== 'active') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Cannot RSVP to inactive events'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if event is in the past
|
||||
const eventDate = new Date(eventDetails.start_datetime);
|
||||
const now = new Date();
|
||||
if (eventDate < now) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Cannot RSVP to past events'
|
||||
});
|
||||
}
|
||||
|
||||
// Get member details for pricing logic
|
||||
const member = await membersClient.findByKeycloakId(session.user.id);
|
||||
if (!member) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Member record not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user already has an RSVP
|
||||
const existingRSVP = await eventsClient.findUserRSVP(eventId, member.member_id || member.Id);
|
||||
|
||||
if (existingRSVP && body.rsvp_status === 'confirmed') {
|
||||
// Update existing RSVP instead of creating new one
|
||||
const updatedRSVP = await eventsClient.updateRSVP(existingRSVP.id, {
|
||||
rsvp_status: body.rsvp_status,
|
||||
rsvp_notes: body.rsvp_notes || '',
|
||||
updated_at: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedRSVP,
|
||||
message: 'RSVP updated successfully'
|
||||
};
|
||||
}
|
||||
|
||||
// Check event capacity if confirming
|
||||
if (body.rsvp_status === 'confirmed') {
|
||||
const isFull = await eventsClient.isEventFull(eventId);
|
||||
if (isFull) {
|
||||
// Add to waitlist instead
|
||||
body.rsvp_status = 'waitlist';
|
||||
}
|
||||
}
|
||||
|
||||
// Determine pricing and payment status
|
||||
let paymentStatus = 'not_required';
|
||||
let isMemberPricing = 'false';
|
||||
|
||||
if (eventDetails.is_paid === 'true' && body.rsvp_status === 'confirmed') {
|
||||
paymentStatus = 'pending';
|
||||
|
||||
// Check if member qualifies for member pricing
|
||||
const isDuesCurrent = member.current_year_dues_paid === 'true';
|
||||
const memberPricingEnabled = eventDetails.member_pricing_enabled === 'true';
|
||||
|
||||
if (isDuesCurrent && memberPricingEnabled) {
|
||||
isMemberPricing = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
// Generate payment reference
|
||||
const paymentReference = eventsClient.generatePaymentReference(
|
||||
member.member_id || member.Id
|
||||
);
|
||||
|
||||
// Create RSVP record
|
||||
const rsvpData = {
|
||||
event_id: eventId,
|
||||
member_id: member.member_id || member.Id,
|
||||
rsvp_status: body.rsvp_status,
|
||||
payment_status: paymentStatus,
|
||||
payment_reference: paymentReference,
|
||||
attended: 'false',
|
||||
rsvp_notes: body.rsvp_notes || '',
|
||||
is_member_pricing: isMemberPricing
|
||||
};
|
||||
|
||||
const newRSVP = await eventsClient.createRSVP(rsvpData);
|
||||
|
||||
// Update event attendee count if confirmed
|
||||
if (body.rsvp_status === 'confirmed') {
|
||||
const currentCount = parseInt(eventDetails.current_attendees) || 0;
|
||||
await eventsClient.updateAttendeeCount(eventId, currentCount + 1);
|
||||
}
|
||||
|
||||
// Include payment information in response for paid events
|
||||
let responseData: any = newRSVP;
|
||||
|
||||
if (eventDetails.is_paid === 'true' && paymentStatus === 'pending') {
|
||||
const registrationConfig = await getRegistrationConfig();
|
||||
|
||||
responseData = {
|
||||
...newRSVP,
|
||||
payment_info: {
|
||||
cost: isMemberPricing === 'true' ?
|
||||
eventDetails.cost_members :
|
||||
eventDetails.cost_non_members,
|
||||
iban: registrationConfig.iban,
|
||||
recipient: registrationConfig.accountHolder,
|
||||
reference: paymentReference,
|
||||
member_pricing: isMemberPricing === 'true'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: responseData,
|
||||
message: body.rsvp_status === 'waitlist' ?
|
||||
'Added to waitlist - event is full' :
|
||||
'RSVP submitted successfully'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing RSVP:', error);
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to process RSVP'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
async function getUserSession(event: any) {
|
||||
try {
|
||||
const sessionCookie = getCookie(event, 'session') || getHeader(event, 'authorization');
|
||||
if (!sessionCookie) return null;
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: 'user-id',
|
||||
tier: 'user'
|
||||
}
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getRegistrationConfig() {
|
||||
// This should fetch from your admin config system
|
||||
return {
|
||||
iban: 'FR76 1234 5678 9012 3456 7890 123',
|
||||
accountHolder: 'MonacoUSA Association'
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user