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})~and(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 and campaign owner functions /** * Get all responses (admin or campaign owners - includes pending/rejected) * Campaign owners only see responses to their own campaigns * @param {Object} req - Express request object * @param {Object} res - Express response object */ async function getAdminResponses(req, res) { try { const { status = 'all', campaign_slug = '', offset = 0, limit = 50 } = req.query; console.log(`User fetching responses with status filter: ${status}, campaign: ${campaign_slug}`); const isAdmin = (req.user && req.user.isAdmin) || req.session?.isAdmin || false; const sessionUserId = req.user?.id ?? req.session?.userId ?? null; const sessionUserEmail = req.user?.email ?? req.session?.userEmail ?? null; // If not admin, we need to filter by campaign ownership let campaignsToFilter = []; if (!isAdmin) { // Get all campaigns owned by this user const allCampaigns = await nocodbService.getAllCampaigns(); campaignsToFilter = allCampaigns.filter(campaign => { const createdById = campaign['Created By User ID'] ?? campaign.created_by_user_id ?? null; const createdByEmail = campaign['Created By User Email'] ?? campaign.created_by_user_email ?? null; return ( (createdById != null && sessionUserId != null && String(createdById) === String(sessionUserId)) || (createdByEmail && sessionUserEmail && String(createdByEmail).toLowerCase() === String(sessionUserEmail).toLowerCase()) ); }); console.log(`User owns ${campaignsToFilter.length} campaigns out of ${allCampaigns.length} total`); } let whereConditions = []; // Filter by status if (status !== 'all') { whereConditions.push(`(Status,eq,${status})`); } // Filter by campaign slug if provided if (campaign_slug) { const slugCondition = `(Campaign Slug,eq,${campaign_slug})`; if (whereConditions.length > 0) { whereConditions[0] += `~and${slugCondition}`; } else { whereConditions.push(slugCondition); } } const responses = await nocodbService.getRepresentativeResponses({ where: whereConditions.length > 0 ? whereConditions[0] : undefined, sort: '-CreatedAt', offset: parseInt(offset), limit: parseInt(limit) }); console.log(`Found ${responses.length} total responses before filtering`); // Filter responses by campaign ownership if not admin let filteredResponses = responses; if (!isAdmin && campaignsToFilter.length > 0) { const ownedCampaignSlugs = campaignsToFilter.map(c => c['Campaign Slug'] || c.slug); filteredResponses = responses.filter(r => ownedCampaignSlugs.includes(r.campaign_slug)); console.log(`After ownership filter: ${filteredResponses.length} responses`); } else if (!isAdmin) { // User owns no campaigns, return empty filteredResponses = []; } if (filteredResponses.length > 0) { console.log('Response statuses:', filteredResponses.map(r => `ID:${r.id} Status:${r.status}`).join(', ')); } res.json({ success: true, responses: filteredResponses, pagination: { offset: parseInt(offset), limit: parseInt(limit), hasMore: filteredResponses.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 or campaign owner) * @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' }); } // Get the response to check campaign ownership const response = await nocodbService.getRepresentativeResponseById(id); if (!response) { return res.status(404).json({ error: 'Response not found' }); } // Get campaign to check ownership const campaignId = response.campaign_id; const campaign = await nocodbService.getCampaignById(campaignId); if (!campaign) { return res.status(404).json({ error: 'Campaign not found' }); } // Check if user is admin or campaign owner const isAdmin = (req.user && req.user.isAdmin) || req.session?.isAdmin || false; const sessionUserId = req.user?.id ?? req.session?.userId ?? null; const sessionUserEmail = req.user?.email ?? req.session?.userEmail ?? null; if (!isAdmin) { const createdById = campaign['Created By User ID'] ?? campaign.created_by_user_id ?? null; const createdByEmail = campaign['Created By User Email'] ?? campaign.created_by_user_email ?? null; const ownsCampaign = ( (createdById != null && sessionUserId != null && String(createdById) === String(sessionUserId)) || (createdByEmail && sessionUserEmail && String(createdByEmail).toLowerCase() === String(sessionUserEmail).toLowerCase()) ); if (!ownsCampaign) { return res.status(403).json({ error: 'You can only moderate responses to your own campaigns' }); } } 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 };