import { Schema, model, Document, Types } from 'mongoose'; interface IGeoPoint { type: "Point"; coordinates: [number, number]; // [lng, lat] } interface IEventParticipant { user: Types.ObjectId; joinedAt: Date; joinMethod: 'code' | 'link' | 'qr' | 'manual'; } export interface IEvent extends Document { // Basic Info title: string; code: string; // Short join code (e.g., NDMA-FIRE-23) theme: string; // e.g., "Fire Safety", "Disaster Response" description?: string; // Organization organization: Types.ObjectId; createdBy: Types.ObjectId; // Trainer who created it // Capacity & Timing capacity: number; startDate: Date; endDate: Date; defaultStartTime: string; // Default session start time (e.g., "16:00") defaultEndTime: string; // Default session end time (e.g., "18:00") // Type & Location type: 'in-person' | 'online' | 'hybrid'; venue?: string; // Physical address for in-person/hybrid location?: IGeoPoint; // Geo coordinates for in-person/hybrid geofenceRadius?: number; // Meters - for GPS check-in onlineLink?: string; // Meeting link for online/hybrid // Join Settings joinToken: string; // Secret token for join links allowSelfJoin: boolean; // Can trainees join without approval? // Participants participants: IEventParticipant[]; waitlist: Types.ObjectId[]; // If capacity is full // Status status: 'draft' | 'published' | 'ongoing' | 'completed' | 'cancelled'; // Notifications reminderSent: boolean; // Metadata createdAt: Date; updatedAt: Date; } const EventSchema = new Schema({ title: { type: String, required: true, trim: true, index: 'text' // For text search }, 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]$/ // HH:MM format }, defaultEndTime: { type: String, required: true, match: /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/ // HH:MM format }, 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], // [lng, lat] 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, // 100 meters default 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 }); // Indexes EventSchema.index({ location: '2dsphere' }); // For geospatial queries EventSchema.index({ startDate: 1, status: 1 }); // For upcoming events EventSchema.index({ organization: 1, status: 1 }); // Org events EventSchema.index({ 'participants.user': 1 }); // User's events // Text search on title and theme EventSchema.index({ title: 'text', theme: 'text', description: 'text' }); // Validation: endDate must be after startDate EventSchema.pre('save', function (this: IEvent) { if (this.endDate <= this.startDate) { throw new Error('End date must be after start date'); } }); // Auto-generate EventDay records when event is created EventSchema.post('save', async function (doc: IEvent) { const { generateEventDays } = await import('../util/eventDay.util'); // Check if this is a new document (not an update) if (this.isNew) { try { await generateEventDays(doc); } catch (err) { console.error('Failed to generate event days:', err); } } }); export default model('Event', EventSchema);