New resposne wall coding started.
This commit is contained in:
467
influence/app/controllers/responses.js
Normal file
467
influence/app/controllers/responses.js
Normal file
@@ -0,0 +1,467 @@
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user