Prashikshak / API /src /model /event.model.ts
Abhisingh-18's picture
Initial commit: Prashikshak - disaster management training platform
9a92a42
Raw
History Blame Contribute Delete
5.16 kB
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<IEvent>({
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<IEvent>('Event', EventSchema);