468 lines
14 KiB
JavaScript
468 lines
14 KiB
JavaScript
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
|
|
};
|