import { Request, Response } from 'express'; import Form from '../model/form.model'; import FormResponse from '../model/formResponse.model'; import Event from '../model/event.model'; import User from '../model/user.model'; import crypto from 'crypto'; import QRCode from 'qrcode'; import { logActivity } from '../util/activity.util'; import { ActivityAction, ActivityCategory } from '../model/userActivity.model'; // Generate unique short code for form const generateShortCode = async (): Promise => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let code: string; let exists = true; while (exists) { code = 'FORM-'; for (let i = 0; i < 6; i++) { code += chars.charAt(Math.floor(Math.random() * chars.length)); } const result = await Form.exists({ shortCode: code }); exists = result !== null; // exists = await Form.exists({ shortCode: code }); } return code!; }; // ==================== CREATE FORM ==================== export const createForm = async (req: Request, res: Response) => { console.log('createForm controller called'); try { const userId = req.userId; console.log('User ID:', userId); const user = await User.findById(userId); if (!user) { return res.status(404).json({ error: 'User not found' }); } // Allow trainers, organizations, and admins const allowedRoles = ['trainer', 'organization', 'admin']; if (!allowedRoles.includes(user.role)) { console.log(`User role ${user.role} not authorized`); return res.status(403).json({ error: 'Unauthorized to create forms' }); } // Check verification for trainers if (user.role === 'trainer' && user.organizationVerificationStatus !== 'verified') { console.log('Trainer not verified'); return res.status(403).json({ error: 'Only verified trainers can create forms' }); } const { title, description, eventId, fields, accessType, restrictToAttendees, oneResponsePerUser, showResponseSummary, allowEditing } = req.body; console.log('Request body:', req.body); // Validate event if provided if (eventId) { const event = await Event.findById(eventId); if (!event) { return res.status(404).json({ error: 'Event not found' }); } // Check if user is creator of event if (event.createdBy.toString() !== userId) { // Admins might be allowed to create forms for any event, but let's keep it strict for now unless requested if (user.role !== 'admin') { return res.status(403).json({ error: 'You can only create forms for your own events' }); } } } // Determine organization let organizationId = user.organization; if (user.role === 'organization') { organizationId = user._id; } // For admins, organizationId might be undefined, which is now allowed // Generate unique identifiers console.log('Generating short code...'); const shortCode = await generateShortCode(); console.log('Short code generated:', shortCode); const shareToken = crypto.randomBytes(32).toString('hex'); // Create form const form = new Form({ title, description, event: eventId || undefined, organization: organizationId, createdBy: userId, fields, accessType: accessType || 'verified_users', restrictToAttendees: restrictToAttendees || false, oneResponsePerUser: oneResponsePerUser !== false, shortCode, shareToken, showResponseSummary: showResponseSummary || false, allowEditing: allowEditing || false, status: 'draft' }); console.log('Saving form...'); await form.save(); console.log('Form saved.'); // Generate QR code console.log('Generating QR code...'); const formUrl = `${process.env.FRONTEND_URL}/forms/${form._id}?token=${shareToken}`; const qrCodeDataUrl = await QRCode.toDataURL(formUrl); console.log('QR code generated.'); form.qrCode = qrCodeDataUrl; await form.save(); // Log activity const activityMetadata: any = { formId: form._id }; if (eventId && form.event) { activityMetadata.eventId = form.event; } await logActivity({ userId: user._id, // Use user._id instead of userId to ensure it's defined action: ActivityAction.FORM_CREATE, category: ActivityCategory.FORM, description: `Created new form: ${form.title}`, metadata: activityMetadata, success: true }); res.status(201).json({ message: 'Form created successfully', form: { id: form._id, title: form.title, shortCode: form.shortCode, shareUrl: formUrl, qrCode: qrCodeDataUrl } }); } catch (error: any) { console.error('Create form error:', error); res.status(500).json({ error: 'Failed to create form' }); } }; // ==================== UPDATE FORM ==================== export const updateForm = async (req: Request, res: Response) => { try { const userId = req.userId; const { formId } = req.params; const user = await User.findById(userId); if (!user) return res.status(404).json({ error: 'User not found' }); const form = await Form.findById(formId); if (!form) { return res.status(404).json({ error: 'Form not found' }); } // Check ownership or admin if (form.createdBy.toString() !== userId && user.role !== 'admin') { return res.status(403).json({ error: 'You can only update your own forms' }); } const { title, description, fields, accessType, restrictToAttendees, oneResponsePerUser, acceptingResponses, showResponseSummary, allowEditing, status } = req.body; // Update fields if (title) form.title = title; if (description !== undefined) form.description = description; if (fields) form.fields = fields; if (accessType) form.accessType = accessType; if (restrictToAttendees !== undefined) form.restrictToAttendees = restrictToAttendees; if (oneResponsePerUser !== undefined) form.oneResponsePerUser = oneResponsePerUser; if (acceptingResponses !== undefined) form.acceptingResponses = acceptingResponses; if (showResponseSummary !== undefined) form.showResponseSummary = showResponseSummary; if (allowEditing !== undefined) form.allowEditing = allowEditing; if (status) form.status = status; await form.save(); res.json({ message: 'Form updated successfully', form }); } catch (error: any) { console.error('Update form error:', error); res.status(500).json({ error: 'Failed to update form' }); } }; // ==================== GET FORM DETAILS ==================== export const getFormDetails = async (req: Request, res: Response) => { try { const userId = req.userId; const { formId } = req.params; const { token } = req.query; const form = await Form.findById(formId) .populate('createdBy', 'username email') .populate('organization', 'username') .populate('event', 'title code startDate'); if (!form) { return res.status(404).json({ error: 'Form not found' }); } // Verify access token for non-creators if (form.createdBy._id.toString() !== userId && token !== form.shareToken) { return res.status(403).json({ error: 'Invalid access token' }); } // Check if user already submitted (if oneResponsePerUser is true) let hasSubmitted = false; if (form.oneResponsePerUser && userId) { const existingResponse = await FormResponse.findOne({ form: formId, respondent: userId }); hasSubmitted = !!existingResponse; } res.json({ form, hasSubmitted, canEdit: form.allowEditing && hasSubmitted }); } catch (error: any) { console.error('Get form details error:', error); res.status(500).json({ error: 'Failed to fetch form details' }); } }; // ==================== SUBMIT FORM RESPONSE ==================== export const submitFormResponse = async (req: Request, res: Response) => { try { const userId = req.userId; const { formId } = req.params; const { responses } = req.body; const user = await User.findById(userId); if (!user) { return res.status(404).json({ error: 'User not found' }); } const form = await Form.findById(formId).populate('event'); if (!form) { return res.status(404).json({ error: 'Form not found' }); } // Check if form is accepting responses if (!form.acceptingResponses || form.status === 'closed') { return res.status(400).json({ error: 'Form is not accepting responses' }); } // Access control checks if (form.accessType === 'verified_users') { // Allow any logged in user (which authMiddleware ensures) // If we want to restrict to "verified" users only (e.g. verified trainers/orgs), we can add checks here. // But user said "all users should be allowed to fill forms". // So we just rely on them being logged in. } // Check if restricted to event attendees if (form.restrictToAttendees && form.event) { const event = form.event as any; const isAttendee = event.participants.some( (p: any) => p.user.toString() === userId ); if (!isAttendee) { return res.status(403).json({ error: 'Only event attendees can submit this form' }); } } // Check if user already submitted (if oneResponsePerUser is true) if (form.oneResponsePerUser) { const existingResponse = await FormResponse.findOne({ form: formId, respondent: userId }); if (existingResponse) { if (!form.allowEditing) { return res.status(400).json({ error: 'You have already submitted this form' }); } // Update existing response existingResponse.responses = responses; existingResponse.lastEditedAt = new Date(); await existingResponse.save(); return res.json({ message: 'Response updated successfully', response: existingResponse }); } } // Validate required fields const requiredFields = form.fields.filter(f => f.required); const responseFieldIds = responses.map((r: any) => r.fieldId); for (const field of requiredFields) { if (!responseFieldIds.includes(field.id)) { return res.status(400).json({ error: `Field "${field.label}" is required` }); } } // Create new response const formResponse = new FormResponse({ form: formId, event: form.event || undefined, respondent: userId, responses, ipAddress: req.ip, userAgent: req.get('user-agent'), submittedAt: new Date(), isComplete: true }); await formResponse.save(); // Increment response count form.responseCount += 1; await form.save(); res.status(201).json({ message: 'Response submitted successfully', response: formResponse }); } catch (error: any) { console.error('Submit form response error:', error); // Handle duplicate response error if (error.code === 11000) { return res.status(400).json({ error: 'You have already submitted this form' }); } res.status(500).json({ error: 'Failed to submit response' }); } }; // ==================== GET FORM RESPONSES ==================== export const getFormResponses = async (req: Request, res: Response) => { try { const userId = req.userId; const { formId } = req.params; const form = await Form.findById(formId); if (!form) { return res.status(404).json({ error: 'Form not found' }); } // Only form creator can view responses if (form.createdBy.toString() !== userId) { return res.status(403).json({ error: 'You can only view responses to your own forms' }); } const responses = await FormResponse.find({ form: formId }) .populate('respondent', 'username email role') .sort({ submittedAt: -1 }); res.json({ form: { title: form.title, responseCount: form.responseCount }, responses }); } catch (error: any) { console.error('Get form responses error:', error); res.status(500).json({ error: 'Failed to fetch responses' }); } }; // ==================== GET MY FORMS ==================== export const getMyForms = async (req: Request, res: Response) => { try { const userId = req.userId; const { eventId } = req.query; const query: any = { createdBy: userId }; if (eventId) { query.event = eventId; } const forms = await Form.find(query) .populate('event', 'title code') .sort({ createdAt: -1 }); res.json({ forms }); } catch (error: any) { console.error('Get my forms error:', error); res.status(500).json({ error: 'Failed to fetch forms' }); } }; // ==================== DELETE FORM ==================== export const deleteForm = async (req: Request, res: Response) => { try { const userId = req.userId; const { formId } = req.params; const user = await User.findById(userId); if (!user) return res.status(404).json({ error: 'User not found' }); const form = await Form.findById(formId); if (!form) { return res.status(404).json({ error: 'Form not found' }); } // Check ownership or admin if (form.createdBy.toString() !== userId && user.role !== 'admin') { return res.status(403).json({ error: 'You can only delete your own forms' }); } // Delete all responses await FormResponse.deleteMany({ form: formId }); // Delete form await form.deleteOne(); res.json({ message: 'Form deleted successfully' }); } catch (error: any) { console.error('Delete form error:', error); res.status(500).json({ error: 'Failed to delete form' }); } }; // ==================== GET MY RESPONSE ==================== export const getMyResponse = async (req: Request, res: Response) => { try { const userId = req.userId; const { formId } = req.params; const response = await FormResponse.findOne({ form: formId, respondent: userId }).populate('form', 'title fields allowEditing'); if (!response) { return res.status(404).json({ error: 'No response found' }); } res.json({ response }); } catch (error: any) { console.error('Get my response error:', error); res.status(500).json({ error: 'Failed to fetch response' }); } }; // ==================== EXPORT RESPONSES TO CSV ==================== export const exportResponsesToCSV = async (req: Request, res: Response) => { try { const userId = req.userId; const { formId } = req.params; const form = await Form.findById(formId); if (!form) { return res.status(404).json({ error: 'Form not found' }); } // Only form creator can export if (form.createdBy.toString() !== userId) { return res.status(403).json({ error: 'Unauthorized' }); } const responses = await FormResponse.find({ form: formId }) .populate('respondent', 'username email') .sort({ submittedAt: 1 }); // Build CSV const headers = ['Timestamp', 'Respondent', 'Email']; form.fields.forEach(field => headers.push(field.label)); const rows = responses.map(response => { const row = [ new Date(response.submittedAt).toISOString(), (response.respondent as any).username, (response.respondent as any).email ]; form.fields.forEach(field => { const fieldResponse = response.responses.find(r => r.fieldId === field.id); row.push(fieldResponse ? JSON.stringify(fieldResponse.value) : ''); }); return row; }); const csv = [headers, ...rows].map(row => row.join(',')).join('\n'); res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', `attachment; filename="${form.title}-responses.csv"`); res.send(csv); } catch (error: any) { console.error('Export CSV error:', error); res.status(500).json({ error: 'Failed to export responses' }); } };