| import { Schema, model, Document, Types } from 'mongoose'; |
|
|
| interface IGeoPoint { |
| type: "Point"; |
| coordinates: [number, number]; |
| } |
|
|
| interface IEventParticipant { |
| user: Types.ObjectId; |
| joinedAt: Date; |
| joinMethod: 'code' | 'link' | 'qr' | 'manual'; |
| } |
|
|
| export interface IEvent extends Document { |
| |
| title: string; |
| code: string; |
| theme: string; |
| description?: string; |
|
|
| |
| organization: Types.ObjectId; |
| createdBy: Types.ObjectId; |
|
|
| |
| capacity: number; |
| startDate: Date; |
| endDate: Date; |
| defaultStartTime: string; |
| defaultEndTime: string; |
|
|
| |
| type: 'in-person' | 'online' | 'hybrid'; |
| venue?: string; |
| location?: IGeoPoint; |
| geofenceRadius?: number; |
| onlineLink?: string; |
|
|
| |
| joinToken: string; |
| allowSelfJoin: boolean; |
|
|
| |
| participants: IEventParticipant[]; |
| waitlist: Types.ObjectId[]; |
|
|
| |
| status: 'draft' | 'published' | 'ongoing' | 'completed' | 'cancelled'; |
|
|
| |
| reminderSent: boolean; |
|
|
| |
| createdAt: Date; |
| updatedAt: Date; |
| } |
|
|
| const EventSchema = new Schema<IEvent>({ |
| title: { |
| type: String, |
| required: true, |
| trim: true, |
| index: 'text' |
| }, |
|
|
| code: { |
| type: String, |
| required: true, |
| unique: true, |
| uppercase: true, |
| trim: true, |
| index: true |
| }, |
|
|
| theme: { |
| type: String, |
| required: true, |
| index: true |
| }, |
|
|
| description: { |
| type: String, |
| trim: true |
| }, |
|
|
| organization: { |
| type: Schema.Types.ObjectId, |
| ref: 'User', |
| required: true, |
| index: true |
| }, |
|
|
| createdBy: { |
| type: Schema.Types.ObjectId, |
| ref: 'User', |
| required: true, |
| index: true |
| }, |
|
|
| capacity: { |
| type: Number, |
| required: true, |
| min: 1 |
| }, |
|
|
| startDate: { |
| type: Date, |
| required: true, |
| index: true |
| }, |
|
|
| endDate: { |
| type: Date, |
| required: true, |
| index: true |
| }, |
|
|
| defaultStartTime: { |
| type: String, |
| required: true, |
| match: /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/ |
| }, |
|
|
| defaultEndTime: { |
| type: String, |
| required: true, |
| match: /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/ |
| }, |
|
|
| type: { |
| type: String, |
| enum: ['in-person', 'online', 'hybrid'], |
| required: true, |
| index: true |
| }, |
|
|
| venue: { |
| type: String, |
| trim: true |
| }, |
|
|
| location: { |
| type: { |
| type: String, |
| enum: ['Point'], |
| default: 'Point' |
| }, |
| coordinates: { |
| type: [Number], |
| validate: { |
| validator: function (v: number[]) { |
| return v.length === 2 && |
| v[0] >= -180 && v[0] <= 180 && |
| v[1] >= -90 && v[1] <= 90; |
| }, |
| message: 'Invalid coordinates' |
| } |
| } |
| }, |
|
|
| geofenceRadius: { |
| type: Number, |
| default: 100, |
| min: 10, |
| max: 5000 |
| }, |
|
|
| onlineLink: { |
| type: String, |
| trim: true |
| }, |
|
|
| joinToken: { |
| type: String, |
| required: true, |
| index: true |
| }, |
|
|
| allowSelfJoin: { |
| type: Boolean, |
| default: true |
| }, |
|
|
| participants: [{ |
| user: { |
| type: Schema.Types.ObjectId, |
| ref: 'User', |
| required: true |
| }, |
| joinedAt: { |
| type: Date, |
| default: Date.now |
| }, |
| joinMethod: { |
| type: String, |
| enum: ['code', 'link', 'qr', 'manual'], |
| required: true |
| } |
| }], |
|
|
| waitlist: [{ |
| type: Schema.Types.ObjectId, |
| ref: 'User' |
| }], |
|
|
| status: { |
| type: String, |
| enum: ['draft', 'published', 'ongoing', 'completed', 'cancelled'], |
| default: 'draft', |
| index: true |
| }, |
|
|
| reminderSent: { |
| type: Boolean, |
| default: false |
| } |
| }, { |
| timestamps: true |
| }); |
|
|
| |
| EventSchema.index({ location: '2dsphere' }); |
| EventSchema.index({ startDate: 1, status: 1 }); |
| EventSchema.index({ organization: 1, status: 1 }); |
| EventSchema.index({ 'participants.user': 1 }); |
|
|
| |
| EventSchema.index({ title: 'text', theme: 'text', description: 'text' }); |
|
|
| |
| EventSchema.pre('save', function (this: IEvent) { |
| if (this.endDate <= this.startDate) { |
| throw new Error('End date must be after start date'); |
| } |
| }); |
|
|
| |
| EventSchema.post('save', async function (doc: IEvent) { |
| const { generateEventDays } = await import('../util/eventDay.util'); |
|
|
| |
| if (this.isNew) { |
| try { |
| await generateEventDays(doc); |
| } catch (err) { |
| console.error('Failed to generate event days:', err); |
| } |
| } |
| }); |
|
|
| export default model<IEvent>('Event', EventSchema); |