| import { Request, Response } from 'express'; |
| import Event, { IEvent } from '../model/event.model'; |
| import EventDay from '../model/eventDay.model'; |
| import User from '../model/user.model'; |
| import crypto from 'crypto'; |
| import sendMail from '../util/mailer.util'; |
| import mongoose from 'mongoose'; |
| import { deleteEventDays } from '../util/eventDay.util'; |
|
|
| interface IEventRequest extends Request { |
| userId?: string; |
| } |
|
|
| |
| const generateEventCode = async (orgName: string, theme: string): Promise<string> => { |
| const orgPrefix = orgName.substring(0, 4).toUpperCase().replace(/[^A-Z]/g, ''); |
| const themePrefix = theme.substring(0, 4).toUpperCase().replace(/[^A-Z]/g, ''); |
| const random = Math.floor(1000 + Math.random() * 9000); |
|
|
| let code = `${orgPrefix}-${themePrefix}-${random}`; |
| let exists = await Event.findOne({ code }); |
|
|
| while (exists) { |
| const newRandom = Math.floor(1000 + Math.random() * 9000); |
| code = `${orgPrefix}-${themePrefix}-${newRandom}`; |
| exists = await Event.findOne({ code }); |
| } |
|
|
| return code; |
| }; |
|
|
| |
| const calculateDistance = ( |
| lat1: number, lon1: number, |
| lat2: number, lon2: number |
| ): number => { |
| const R = 6371e3; |
| const φ1 = lat1 * Math.PI / 180; |
| const φ2 = lat2 * Math.PI / 180; |
| const Δφ = (lat2 - lat1) * Math.PI / 180; |
| const Δλ = (lon2 - lon1) * Math.PI / 180; |
|
|
| const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + |
| Math.cos(φ1) * Math.cos(φ2) * |
| Math.sin(Δλ / 2) * Math.sin(Δλ / 2); |
| const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); |
|
|
| return R * c; |
| }; |
|
|
| |
| export const createEvent = async (req: IEventRequest, res: Response) => { |
| try { |
| const userId = req.userId; |
| const user = await User.findById(userId).populate('organization'); |
|
|
| if (!user) { |
| return res.status(404).json({ message: 'User not found' }); |
| } |
|
|
| |
| const allowedRoles = ['trainer', 'organization', 'admin']; |
| if (!allowedRoles.includes(user.role)) { |
| return res.status(403).json({ |
| message: 'Only trainers, organizations, and admins can create events' |
| }); |
| } |
|
|
| |
| if (user.role === 'trainer') { |
| |
| if (user.organizationVerificationStatus !== 'verified') { |
| return res.status(403).json({ |
| message: 'Only verified trainers can create events', |
| verificationStatus: user.organizationVerificationStatus |
| }); |
| } |
| } else if (user.role === 'organization') { |
| |
| if (user.verificationStatus !== 'approved') { |
| return res.status(403).json({ |
| message: 'Only verified organizations can create events', |
| verificationStatus: user.verificationStatus |
| }); |
| } |
| } |
| |
|
|
| const { |
| title, theme, description, capacity, |
| startDate, endDate, type, venue, |
| longitude, latitude, geofenceRadius, |
| onlineLink, allowSelfJoin = true, |
| defaultStartTime = "09:00", |
| defaultEndTime = "17:00" |
| } = req.body; |
|
|
| |
| if (!title || !theme || !capacity || !startDate || !endDate || !type) { |
| return res.status(400).json({ message: 'Missing required fields' }); |
| } |
|
|
| if (new Date(endDate) <= new Date(startDate)) { |
| return res.status(400).json({ message: 'End date must be after start date' }); |
| } |
|
|
| |
| const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/; |
| if (!timeRegex.test(defaultStartTime) || !timeRegex.test(defaultEndTime)) { |
| return res.status(400).json({ message: 'Invalid time format. Use HH:MM (e.g., 16:00)' }); |
| } |
|
|
| |
| const [startHour, startMin] = defaultStartTime.split(':').map(Number); |
| const [endHour, endMin] = defaultEndTime.split(':').map(Number); |
| if (endHour < startHour || (endHour === startHour && endMin <= startMin)) { |
| return res.status(400).json({ message: 'End time must be after start time' }); |
| } |
|
|
| if ((type === 'in-person' || type === 'hybrid') && (!longitude || !latitude || !venue)) { |
| return res.status(400).json({ |
| message: 'Location (longitude, latitude) and venue required for in-person/hybrid events' |
| }); |
| } |
|
|
| if ((type === 'online' || type === 'hybrid') && !onlineLink) { |
| return res.status(400).json({ message: 'Online link required for online/hybrid events' }); |
| } |
|
|
| |
| |
| let eventOrganization: any; |
| let orgName: string; |
|
|
| if (user.role === 'trainer') { |
| |
| eventOrganization = user.organization; |
| orgName = (user.organization as any)?.username || 'ORG'; |
| } else if (user.role === 'organization') { |
| |
| eventOrganization = userId; |
| orgName = user.username || 'ORG'; |
| } else { |
| |
| eventOrganization = user.organization || userId; |
| orgName = user.organization ? (user.organization as any)?.username : user.username || 'ORG'; |
| } |
|
|
| const code = await generateEventCode(orgName, theme); |
| const joinToken = crypto.randomBytes(32).toString('hex'); |
|
|
| const eventData: any = { |
| title, |
| code, |
| theme, |
| description, |
| organization: eventOrganization, |
| createdBy: userId, |
| capacity, |
| startDate: new Date(startDate), |
| endDate: new Date(endDate), |
| defaultStartTime, |
| defaultEndTime, |
| type, |
| joinToken, |
| allowSelfJoin, |
| status: 'published' |
| }; |
|
|
| if (type === 'in-person' || type === 'hybrid') { |
| eventData.venue = venue; |
| eventData.location = { |
| type: 'Point', |
| coordinates: [parseFloat(longitude), parseFloat(latitude)] |
| }; |
| eventData.geofenceRadius = geofenceRadius || 100; |
| } |
|
|
| if (type === 'online' || type === 'hybrid') { |
| eventData.onlineLink = onlineLink; |
| } |
|
|
| const event = new Event(eventData); |
| await event.save(); |
|
|
| return res.status(201).json({ |
| message: 'Event created successfully', |
| event: { |
| _id: event._id, |
| title: event.title, |
| code: event.code, |
| theme: event.theme, |
| startDate: event.startDate, |
| endDate: event.endDate, |
| type: event.type, |
| capacity: event.capacity, |
| joinToken: event.joinToken |
| } |
| }); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |
|
|
| |
| export const searchEvents = async (req: IEventRequest, res: Response) => { |
| try { |
| const { |
| query, |
| theme, |
| organization, |
| type, |
| status = 'published', |
| startDate, |
| endDate, |
| page = 1, |
| limit = 20 |
| } = req.query; |
|
|
| const filter: any = { status }; |
|
|
| |
| if (query) { |
| filter.$text = { $search: query as string }; |
| } |
|
|
| if (theme) filter.theme = theme; |
| if (organization) filter.organization = organization; |
| if (type) filter.type = type; |
|
|
| |
| if (startDate || endDate) { |
| filter.startDate = {}; |
| if (startDate) filter.startDate.$gte = new Date(startDate as string); |
| if (endDate) filter.startDate.$lte = new Date(endDate as string); |
| } |
|
|
| const skip = (Number(page) - 1) * Number(limit); |
|
|
| const events = await Event.find(filter) |
| .populate('organization', 'username organizationType') |
| .populate('createdBy', 'username profilePhoto') |
| .select('-joinToken -participants.attendance') |
| .sort({ startDate: 1 }) |
| .skip(skip) |
| .limit(Number(limit)); |
|
|
| const total = await Event.countDocuments(filter); |
|
|
| |
| const eventsWithCount = events.map(event => ({ |
| ...event.toObject(), |
| participantCount: event.participants.length, |
| isCapacityFull: event.participants.length >= event.capacity |
| })); |
|
|
| return res.json({ |
| events: eventsWithCount, |
| pagination: { |
| total, |
| page: Number(page), |
| limit: Number(limit), |
| totalPages: Math.ceil(total / Number(limit)) |
| } |
| }); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |
|
|
| |
| export const getNearbyEvents = async (req: IEventRequest, res: Response) => { |
| try { |
| const { longitude, latitude, maxDistance = 50000 } = req.query; |
|
|
| if (!longitude || !latitude) { |
| return res.status(400).json({ message: 'Location (longitude, latitude) required' }); |
| } |
|
|
| const events = await Event.find({ |
| status: 'published', |
| location: { |
| $near: { |
| $geometry: { |
| type: 'Point', |
| coordinates: [parseFloat(longitude as string), parseFloat(latitude as string)] |
| }, |
| $maxDistance: Number(maxDistance) |
| } |
| }, |
| startDate: { $gte: new Date() } |
| }) |
| .populate('organization', 'username organizationType') |
| .populate('createdBy', 'username profilePhoto') |
| .select('-joinToken -participants.attendance') |
| .limit(20); |
|
|
| const eventsWithDistance = events.map(event => { |
| const distance = event.location ? calculateDistance( |
| parseFloat(latitude as string), |
| parseFloat(longitude as string), |
| event.location.coordinates[1], |
| event.location.coordinates[0] |
| ) : null; |
|
|
| return { |
| ...event.toObject(), |
| distance: distance ? Math.round(distance) : null, |
| participantCount: event.participants.length, |
| isCapacityFull: event.participants.length >= event.capacity |
| }; |
| }); |
|
|
| return res.json({ events: eventsWithDistance }); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |
|
|
| |
| export const getEventDashboard = async (req: IEventRequest, res: Response) => { |
| try { |
| const userId = req.userId; |
| const now = new Date(); |
|
|
| |
| const upcomingEvents = await Event.find({ |
| 'participants.user': userId, |
| startDate: { $gt: now }, |
| status: 'published' |
| }) |
| .populate('organization', 'username') |
| .sort({ startDate: 1 }) |
| .limit(10); |
|
|
| |
| const pastEvents = await Event.find({ |
| 'participants.user': userId, |
| endDate: { $lt: now }, |
| status: { $in: ['completed', 'published'] } |
| }) |
| .populate('organization', 'username') |
| .sort({ endDate: -1 }) |
| .limit(10); |
|
|
| |
| const userThemes = [...new Set(pastEvents.map(e => e.theme))]; |
| const recommended = await Event.find({ |
| status: 'published', |
| startDate: { $gt: now }, |
| theme: { $in: userThemes }, |
| 'participants.user': { $ne: userId } |
| }) |
| .populate('organization', 'username') |
| .limit(5); |
|
|
| return res.json({ |
| upcoming: upcomingEvents.map(e => ({ |
| ...e.toObject(), |
| participantCount: e.participants.length |
| })), |
| past: pastEvents.map(e => { |
| const participantCount = e.participants.length; |
| return { |
| ...e.toObject(), |
| participantCount |
| }; |
| }), |
| recommended: recommended.map(e => ({ |
| ...e.toObject(), |
| participantCount: e.participants.length |
| })) |
| }); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |
|
|
| |
| export const joinEventByCode = async (req: IEventRequest, res: Response) => { |
| try { |
| const userId = req.userId; |
| const { code } = req.body; |
|
|
| if (!code) { |
| return res.status(400).json({ message: 'Event code required' }); |
| } |
|
|
| const event = await Event.findOne({ code: code.toUpperCase(), status: 'published' }); |
|
|
| if (!event) { |
| return res.status(404).json({ message: 'Event not found or not published' }); |
| } |
|
|
| |
| if (event.participants.find(p => p.user.toString() === userId)) { |
| return res.status(400).json({ message: 'Already joined this event' }); |
| } |
|
|
| |
| if (event.participants.length >= event.capacity) { |
| |
| if (!event.waitlist.includes(userId as any)) { |
| event.waitlist.push(userId as any); |
| await event.save(); |
| } |
| return res.status(400).json({ |
| message: 'Event is full. You have been added to the waitlist.', |
| waitlisted: true |
| }); |
| } |
|
|
| |
| event.participants.push({ |
| user: userId as any, |
| joinedAt: new Date(), |
| joinMethod: 'code' |
| } as any); |
|
|
| await event.save(); |
|
|
| |
| const user = await User.findById(userId); |
| if (user) { |
| await sendMail({ |
| to: user.email, |
| subject: `Registered for ${event.title}`, |
| text: `You have successfully registered for ${event.title} (${event.code})`, |
| html: `<p>You have successfully registered for <strong>${event.title}</strong></p> |
| <p>Code: ${event.code}</p> |
| <p>Start: ${event.startDate.toLocaleString()}</p>` |
| }); |
| } |
|
|
| return res.json({ |
| message: 'Successfully joined event', |
| event: { |
| _id: event._id, |
| title: event.title, |
| code: event.code, |
| startDate: event.startDate, |
| endDate: event.endDate, |
| type: event.type, |
| venue: event.venue, |
| onlineLink: event.onlineLink |
| } |
| }); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |
|
|
| |
| export const joinEventByLink = async (req: IEventRequest, res: Response) => { |
| try { |
| const userId = req.userId; |
| const { eventId, token } = req.body; |
|
|
| if (!eventId || !token) { |
| return res.status(400).json({ message: 'Event ID and token required' }); |
| } |
|
|
| const event = await Event.findOne({ |
| _id: eventId, |
| joinToken: token, |
| status: 'published' |
| }); |
|
|
| if (!event) { |
| return res.status(404).json({ message: 'Invalid event link' }); |
| } |
|
|
| |
| if (event.participants.find(p => p.user.toString() === userId)) { |
| return res.status(400).json({ message: 'Already joined this event' }); |
| } |
|
|
| |
| if (event.participants.length >= event.capacity) { |
| if (!event.waitlist.includes(userId as any)) { |
| event.waitlist.push(userId as any); |
| await event.save(); |
| } |
| return res.status(400).json({ |
| message: 'Event is full. You have been added to the waitlist.', |
| waitlisted: true |
| }); |
| } |
|
|
| |
| event.participants.push({ |
| user: userId as any, |
| joinedAt: new Date(), |
| joinMethod: 'link' |
| } as any); |
|
|
| await event.save(); |
|
|
| return res.json({ |
| message: 'Successfully joined event', |
| event: { |
| _id: event._id, |
| title: event.title, |
| code: event.code, |
| startDate: event.startDate, |
| endDate: event.endDate |
| } |
| }); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |
|
|
| |
| export const getEventDetails = async (req: IEventRequest, res: Response) => { |
| try { |
| const { eventId } = req.params; |
| const userId = req.userId; |
|
|
| const event = await Event.findById(eventId) |
| .populate('organization', 'username email organizationType') |
| .populate('createdBy', 'username email profilePhoto workDesignation') |
| .populate('participants.user', 'username profilePhoto'); |
|
|
| if (!event) { |
| return res.status(404).json({ message: 'Event not found' }); |
| } |
|
|
| const isParticipant = event.participants.find(p => p.user._id.toString() === userId); |
| const isCreator = event.createdBy._id.toString() === userId; |
|
|
| const response: any = { |
| ...event.toObject(), |
| participantCount: event.participants.length, |
| isCapacityFull: event.participants.length >= event.capacity, |
| isJoined: !!isParticipant, |
| isCreator |
| }; |
|
|
| |
| if (!isCreator) { |
| delete response.joinToken; |
| } |
|
|
| |
| if (!isParticipant && !isCreator) { |
| response.participants = undefined; |
| } |
|
|
| return res.json(response); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |
|
|
| |
| export const getTrainerEvents = async (req: IEventRequest, res: Response) => { |
| try { |
| const creatorId = req.userId; |
| const { status } = req.query; |
|
|
| const filter: any = { createdBy: creatorId }; |
| if (status) filter.status = status; |
|
|
| const events = await Event.find(filter) |
| .populate('organization', 'username') |
| .sort({ startDate: -1 }); |
|
|
| |
| const eventsWithStats = await Promise.all(events.map(async event => { |
| const eventDays = await EventDay.find({ event: event._id }); |
| const totalDays = eventDays.length; |
| const completedDays = eventDays.filter(day => day.isCompleted).length; |
|
|
| |
| const totalAttendance = eventDays.reduce((sum, day) => sum + day.attendance.length, 0); |
| const averageAttendancePerDay = totalDays > 0 ? totalAttendance / totalDays : 0; |
|
|
| return { |
| ...event.toObject(), |
| participantCount: event.participants.length, |
| totalDays, |
| completedDays, |
| averageAttendancePerDay: Math.round(averageAttendancePerDay * 100) / 100, |
| waitlistCount: event.waitlist.length |
| }; |
| })); |
|
|
| return res.json({ events: eventsWithStats }); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |
|
|
| |
| export const updateEvent = async (req: IEventRequest, res: Response) => { |
| try { |
| const creatorId = req.userId; |
| const { eventId } = req.params; |
| const updates = req.body; |
|
|
| const event = await Event.findOne({ _id: eventId, createdBy: creatorId }); |
|
|
| if (!event) { |
| return res.status(404).json({ message: 'Event not found or unauthorized' }); |
| } |
|
|
| |
| delete updates.code; |
| delete updates.joinToken; |
| delete updates.organization; |
| delete updates.createdBy; |
| delete updates.participants; |
|
|
| Object.assign(event, updates); |
| await event.save(); |
|
|
| return res.json({ |
| message: 'Event updated successfully', |
| event |
| }); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |
|
|
| |
| export const deleteEvent = async (req: IEventRequest, res: Response) => { |
| try { |
| const creatorId = req.userId; |
| const { eventId } = req.params; |
|
|
| const event = await Event.findOne({ |
| _id: eventId, |
| createdBy: creatorId |
| }); |
|
|
| if (!event) { |
| return res.status(404).json({ message: 'Event not found or unauthorized' }); |
| } |
|
|
| |
| await deleteEventDays(eventId); |
|
|
| |
| await Event.findByIdAndDelete(eventId); |
|
|
| return res.json({ message: 'Event deleted successfully' }); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |
|
|
| |
| export const getOrganizationEventStats = async (req: IEventRequest, res: Response) => { |
| try { |
| const { organizationId } = req.params; |
|
|
| |
| const endDate = new Date(); |
| const startDate = new Date(); |
| startDate.setFullYear(startDate.getFullYear() - 1); |
|
|
| const events = await Event.aggregate([ |
| { |
| $match: { |
| organization: new mongoose.Types.ObjectId(organizationId), |
| startDate: { $gte: startDate, $lte: endDate }, |
| status: { $in: ['published', 'ongoing', 'completed'] } |
| } |
| }, |
| { |
| $group: { |
| _id: { $dateToString: { format: "%Y-%m-%d", date: "$startDate" } }, |
| count: { $sum: 1 } |
| } |
| }, |
| { |
| $project: { |
| date: "$_id", |
| count: 1, |
| _id: 0 |
| } |
| } |
| ]); |
|
|
| return res.json({ stats: events }); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |
|
|
| |
| |
| export const getLiveEvents = async (req: IEventRequest, res: Response) => { |
| try { |
| const now = new Date(); |
|
|
| |
| const startedTrainingDays = await EventDay.find({ |
| hasStarted: true, |
| isCompleted: false |
| }).distinct('event'); |
|
|
| |
| const ongoingEvents = await Event.find({ |
| status: { $in: ['published', 'ongoing'] }, |
| startDate: { $lte: now }, |
| endDate: { $gte: now }, |
| _id: { $nin: startedTrainingDays }, |
| location: { $exists: true, $ne: null } |
| }) |
| .populate('organization', 'username organizationType') |
| .populate('createdBy', 'username profilePhoto') |
| .select('-joinToken'); |
|
|
| const eventsWithCount = ongoingEvents.map(event => ({ |
| ...event.toObject(), |
| participantCount: event.participants.length, |
| isCapacityFull: event.participants.length >= event.capacity |
| })); |
|
|
| return res.json({ events: eventsWithCount }); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |
|
|
| |
| export const getLiveTrainingEvents = async (req: IEventRequest, res: Response) => { |
| try { |
| const now = new Date(); |
|
|
| |
| const liveTrainingDays = await EventDay.find({ |
| hasStarted: true, |
| isCompleted: false |
| }).populate({ |
| path: 'event', |
| populate: [ |
| { path: 'organization', select: 'username organizationType' }, |
| { path: 'createdBy', select: 'username profilePhoto' } |
| ] |
| }); |
|
|
| |
| const liveTrainingEvents = liveTrainingDays |
| .filter(eventDay => { |
| const event = eventDay.event as any; |
| |
| return event && event.location && event.location.coordinates; |
| }) |
| .map(eventDay => { |
| const event = eventDay.event as any; |
| return { |
| ...event.toObject(), |
| currentEventDay: { |
| _id: eventDay._id, |
| date: eventDay.date, |
| dayNumber: eventDay.dayNumber, |
| startTime: eventDay.startTime, |
| endTime: eventDay.endTime, |
| hasStarted: eventDay.hasStarted, |
| isCompleted: eventDay.isCompleted |
| }, |
| participantCount: event.participants.length, |
| isCapacityFull: event.participants.length >= event.capacity, |
| isLiveTraining: true |
| }; |
| }); |
|
|
| |
| const uniqueEvents = Array.from( |
| new Map(liveTrainingEvents.map(event => [event._id.toString(), event])).values() |
| ); |
|
|
| return res.json({ events: uniqueEvents }); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |
|
|
| |
| export const getAllEventsForAdmin = async (req: IEventRequest, res: Response) => { |
| try { |
| const { status, page = 1, limit = 20 } = req.query; |
|
|
| const filter: any = {}; |
| if (status) filter.status = status; |
|
|
| const skip = (Number(page) - 1) * Number(limit); |
|
|
| const events = await Event.find(filter) |
| .populate('organization', 'username organizationType') |
| .populate('createdBy', 'username profilePhoto') |
| .sort({ createdAt: -1 }) |
| .skip(skip) |
| .limit(Number(limit)); |
|
|
| const total = await Event.countDocuments(filter); |
|
|
| |
| const eventsWithStats = await Promise.all(events.map(async event => { |
| const eventDays = await EventDay.find({ event: event._id }); |
| const totalDays = eventDays.length; |
| const completedDays = eventDays.filter(day => day.isCompleted).length; |
| const liveDays = eventDays.filter(day => { |
| const now = new Date(); |
| return day.hasStarted && !day.isCompleted && |
| now >= day.startTime && now <= day.endTime; |
| }).length; |
|
|
| return { |
| ...event.toObject(), |
| participantCount: event.participants.length, |
| totalDays, |
| completedDays, |
| liveDays, |
| waitlistCount: event.waitlist.length |
| }; |
| })); |
|
|
| return res.json({ |
| events: eventsWithStats, |
| pagination: { |
| total, |
| page: Number(page), |
| limit: Number(limit), |
| totalPages: Math.ceil(total / Number(limit)) |
| } |
| }); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |