const nocodbService = require('../services/nocodb'); const { validateResponse } = require('../utils/validators'); /** * Get all responses for a campaign * Public endpoint - no authentication required * @param {Object} req - Express request object * @param {Object} res - Express response object */ async function getCampaignResponses(req, res) { try { const { slug } = req.params; const { sort = 'recent', level = '', offset = 0, limit = 20, status = 'approved' } = req.query; // Get campaign by slug first const campaign = await nocodbService.getCampaignBySlug(slug); if (!campaign) { return res.status(404).json({ error: 'Campaign not found' }); } // Build filter conditions // NocoDB v2 API requires ~and operator between multiple conditions let whereConditions = [ `(Campaign Slug,eq,${slug})~and(Status,eq,${status})` ]; if (level) { whereConditions[0] += `~and(Representative Level,eq,${level})`; } // Build sort order let sortOrder = '-CreatedAt'; // Default to most recent if (sort === 'upvotes') { sortOrder = '-Upvote Count'; } else if (sort === 'verified') { sortOrder = '-Is Verified,-CreatedAt'; } // Fetch responses const responses = await nocodbService.getRepresentativeResponses({ where: whereConditions[0], // Use the first (and only) element sort: sortOrder, offset: parseInt(offset), limit: parseInt(limit) }); console.log(`Found ${responses.length} responses for campaign ${slug} with status ${status}`); if (responses.length > 0) { console.log('First response sample:', JSON.stringify(responses[0], null, 2)); } // For each response, check if current user has upvoted const userId = req.user?.id; const userEmail = req.user?.email; const responsesWithUpvoteStatus = await Promise.all(responses.map(async (response) => { let hasUpvoted = false; if (userId || userEmail) { const upvotes = await nocodbService.getResponseUpvotes({ where: `(Response ID,eq,${response.id})` }); hasUpvoted = upvotes.some(upvote => (userId && upvote.user_id === userId) || (userEmail && upvote.user_email === userEmail) ); } return { ...response, hasUpvoted }; })); res.json({ success: true, responses: responsesWithUpvoteStatus, pagination: { offset: parseInt(offset), limit: parseInt(limit), hasMore: responses.length === parseInt(limit) } }); } catch (error) { console.error('Error getting campaign responses:', error); res.status(500).json({ error: 'Failed to get campaign responses' }); } } /** * Submit a new response * Optional authentication - allows anonymous submissions * @param {Object} req - Express request object * @param {Object} res - Express response object */ async function submitResponse(req, res) { try { const { slug } = req.params; const responseData = req.body; // Get campaign by slug first const campaign = await nocodbService.getCampaignBySlug(slug); if (!campaign) { return res.status(404).json({ error: 'Campaign not found' }); } // Validate response data const validation = validateResponse(responseData); if (!validation.valid) { return res.status(400).json({ error: validation.error }); } // Handle file upload if present let screenshotUrl = null; if (req.file) { screenshotUrl = `/uploads/responses/${req.file.filename}`; } // Prepare response data for NocoDB const newResponse = { campaign_id: campaign.ID || campaign.Id || campaign.id || campaign['Campaign ID'], campaign_slug: slug, representative_name: responseData.representative_name, representative_title: responseData.representative_title || null, representative_level: responseData.representative_level, response_type: responseData.response_type, response_text: responseData.response_text, user_comment: responseData.user_comment || null, screenshot_url: screenshotUrl, submitted_by_name: responseData.submitted_by_name || null, submitted_by_email: responseData.submitted_by_email || null, submitted_by_user_id: req.user?.id || null, is_anonymous: responseData.is_anonymous || false, status: 'pending', // All submissions start as pending is_verified: false, upvote_count: 0, submitted_ip: req.ip || req.connection.remoteAddress }; console.log('Submitting response with campaign_id:', newResponse.campaign_id, 'from campaign:', campaign); // Create response in database const createdResponse = await nocodbService.createRepresentativeResponse(newResponse); res.status(201).json({ success: true, message: 'Response submitted successfully. It will be visible after moderation.', response: createdResponse }); } catch (error) { console.error('Error submitting response:', error); res.status(500).json({ error: 'Failed to submit response' }); } } /** * Upvote a response * Optional authentication - allows anonymous upvotes with IP tracking * @param {Object} req - Express request object * @param {Object} res - Express response object */ async function upvoteResponse(req, res) { try { const { id } = req.params; // Get response const response = await nocodbService.getRepresentativeResponseById(id); if (!response) { return res.status(404).json({ error: 'Response not found' }); } const userId = req.user?.id; const userEmail = req.user?.email; const userIp = req.ip || req.connection.remoteAddress; // Check if already upvoted const existingUpvotes = await nocodbService.getResponseUpvotes({ where: `(Response ID,eq,${id})` }); const alreadyUpvoted = existingUpvotes.some(upvote => (userId && upvote.user_id === userId) || (userEmail && upvote.user_email === userEmail) || (upvote.upvoted_ip === userIp) ); if (alreadyUpvoted) { return res.status(400).json({ error: 'You have already upvoted this response' }); } // Create upvote await nocodbService.createResponseUpvote({ response_id: id, user_id: userId || null, user_email: userEmail || null, upvoted_ip: userIp }); // Increment upvote count const newCount = (response.upvote_count || 0) + 1; await nocodbService.updateRepresentativeResponse(id, { upvote_count: newCount }); res.json({ success: true, message: 'Response upvoted successfully', upvoteCount: newCount }); } catch (error) { console.error('Error upvoting response:', error); res.status(500).json({ error: 'Failed to upvote response' }); } } /** * Remove upvote from a response * Optional authentication * @param {Object} req - Express request object * @param {Object} res - Express response object */ async function removeUpvote(req, res) { try { const { id } = req.params; // Get response const response = await nocodbService.getRepresentativeResponseById(id); if (!response) { return res.status(404).json({ error: 'Response not found' }); } const userId = req.user?.id; const userEmail = req.user?.email; const userIp = req.ip || req.connection.remoteAddress; // Find upvote to remove const upvotes = await nocodbService.getResponseUpvotes({ where: `(Response ID,eq,${id})` }); const upvoteToRemove = upvotes.find(upvote => (userId && upvote.user_id === userId) || (userEmail && upvote.user_email === userEmail) || (upvote.upvoted_ip === userIp) ); if (!upvoteToRemove) { return res.status(400).json({ error: 'You have not upvoted this response' }); } // Delete upvote await nocodbService.deleteResponseUpvote(upvoteToRemove.id); // Decrement upvote count const newCount = Math.max((response.upvote_count || 0) - 1, 0); await nocodbService.updateRepresentativeResponse(id, { upvote_count: newCount }); res.json({ success: true, message: 'Upvote removed successfully', upvoteCount: newCount }); } catch (error) { console.error('Error removing upvote:', error); res.status(500).json({ error: 'Failed to remove upvote' }); } } /** * Get response statistics for a campaign * Public endpoint * @param {Object} req - Express request object * @param {Object} res - Express response object */ async function getResponseStats(req, res) { try { const { slug } = req.params; // Get campaign by slug first const campaign = await nocodbService.getCampaignBySlug(slug); if (!campaign) { return res.status(404).json({ error: 'Campaign not found' }); } // Get all approved responses for this campaign const responses = await nocodbService.getRepresentativeResponses({ where: `(Campaign Slug,eq,${slug}),(Status,eq,approved)` }); // Calculate stats const totalResponses = responses.length; const verifiedResponses = responses.filter(r => r.is_verified).length; const totalUpvotes = responses.reduce((sum, r) => sum + (r.upvote_count || 0), 0); // Count by level const byLevel = { Federal: responses.filter(r => r.representative_level === 'Federal').length, Provincial: responses.filter(r => r.representative_level === 'Provincial').length, Municipal: responses.filter(r => r.representative_level === 'Municipal').length, 'School Board': responses.filter(r => r.representative_level === 'School Board').length }; res.json({ success: true, stats: { totalResponses, verifiedResponses, totalUpvotes, byLevel } }); } catch (error) { console.error('Error getting response stats:', error); res.status(500).json({ error: 'Failed to get response statistics' }); } } // Admin functions /** * Get all responses (admin only - includes pending/rejected) * @param {Object} req - Express request object * @param {Object} res - Express response object */ async function getAdminResponses(req, res) { try { const { status = 'all', offset = 0, limit = 50 } = req.query; console.log(`Admin fetching responses with status filter: ${status}`); let whereConditions = []; if (status !== 'all') { whereConditions.push(`(Status,eq,${status})`); } const responses = await nocodbService.getRepresentativeResponses({ where: whereConditions.join(','), sort: '-CreatedAt', offset: parseInt(offset), limit: parseInt(limit) }); console.log(`Admin found ${responses.length} total responses`); if (responses.length > 0) { console.log('Response statuses:', responses.map(r => `ID:${r.id} Status:${r.status}`).join(', ')); } res.json({ success: true, responses, pagination: { offset: parseInt(offset), limit: parseInt(limit), hasMore: responses.length === parseInt(limit) } }); } catch (error) { console.error('Error getting admin responses:', error); res.status(500).json({ error: 'Failed to get responses' }); } } /** * Update response status (admin only) * @param {Object} req - Express request object * @param {Object} res - Express response object */ async function updateResponseStatus(req, res) { try { const { id } = req.params; const { status } = req.body; console.log(`Updating response ${id} to status: ${status}`); if (!['pending', 'approved', 'rejected'].includes(status)) { return res.status(400).json({ error: 'Invalid status' }); } const updated = await nocodbService.updateRepresentativeResponse(id, { status }); console.log('Updated response:', JSON.stringify(updated, null, 2)); res.json({ success: true, message: `Response ${status} successfully` }); } catch (error) { console.error('Error updating response status:', error); res.status(500).json({ error: 'Failed to update response status' }); } } /** * Update response (admin only) * @param {Object} req - Express request object * @param {Object} res - Express response object */ async function updateResponse(req, res) { try { const { id } = req.params; const updates = req.body; await nocodbService.updateRepresentativeResponse(id, updates); res.json({ success: true, message: 'Response updated successfully' }); } catch (error) { console.error('Error updating response:', error); res.status(500).json({ error: 'Failed to update response' }); } } /** * Delete response (admin only) * @param {Object} req - Express request object * @param {Object} res - Express response object */ async function deleteResponse(req, res) { try { const { id } = req.params; // Delete all upvotes for this response first const upvotes = await nocodbService.getResponseUpvotes({ where: `(Response ID,eq,${id})` }); for (const upvote of upvotes) { await nocodbService.deleteResponseUpvote(upvote.id); } // Delete the response await nocodbService.deleteRepresentativeResponse(id); res.json({ success: true, message: 'Response deleted successfully' }); } catch (error) { console.error('Error deleting response:', error); res.status(500).json({ error: 'Failed to delete response' }); } } module.exports = { getCampaignResponses, submitResponse, upvoteResponse, removeUpvote, getResponseStats, getAdminResponses, updateResponseStatus, updateResponse, deleteResponse };