Response wall build out

This commit is contained in:
admin 2025-10-11 22:56:48 -06:00
parent ccececaf25
commit 9da13d6d3d
7 changed files with 556 additions and 27 deletions

View File

@ -291,7 +291,7 @@ async function getResponseStats(req, res) {
// Get all approved responses for this campaign // Get all approved responses for this campaign
const responses = await nocodbService.getRepresentativeResponses({ const responses = await nocodbService.getRepresentativeResponses({
where: `(Campaign Slug,eq,${slug}),(Status,eq,approved)` where: `(Campaign Slug,eq,${slug})~and(Status,eq,approved)`
}); });
// Calculate stats // Calculate stats
@ -323,43 +323,91 @@ async function getResponseStats(req, res) {
} }
} }
// Admin functions // Admin and campaign owner functions
/** /**
* Get all responses (admin only - includes pending/rejected) * 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} req - Express request object
* @param {Object} res - Express response object * @param {Object} res - Express response object
*/ */
async function getAdminResponses(req, res) { async function getAdminResponses(req, res) {
try { try {
const { status = 'all', offset = 0, limit = 50 } = req.query; const { status = 'all', campaign_slug = '', offset = 0, limit = 50 } = req.query;
console.log(`Admin fetching responses with status filter: ${status}`); 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 = []; let whereConditions = [];
// Filter by status
if (status !== 'all') { if (status !== 'all') {
whereConditions.push(`(Status,eq,${status})`); 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({ const responses = await nocodbService.getRepresentativeResponses({
where: whereConditions.join(','), where: whereConditions.length > 0 ? whereConditions[0] : undefined,
sort: '-CreatedAt', sort: '-CreatedAt',
offset: parseInt(offset), offset: parseInt(offset),
limit: parseInt(limit) limit: parseInt(limit)
}); });
console.log(`Admin found ${responses.length} total responses`); console.log(`Found ${responses.length} total responses before filtering`);
if (responses.length > 0) {
console.log('Response statuses:', responses.map(r => `ID:${r.id} Status:${r.status}`).join(', ')); // 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({ res.json({
success: true, success: true,
responses, responses: filteredResponses,
pagination: { pagination: {
offset: parseInt(offset), offset: parseInt(offset),
limit: parseInt(limit), limit: parseInt(limit),
hasMore: responses.length === parseInt(limit) hasMore: filteredResponses.length === parseInt(limit)
} }
}); });
@ -370,7 +418,7 @@ async function getAdminResponses(req, res) {
} }
/** /**
* Update response status (admin only) * Update response status (admin or campaign owner)
* @param {Object} req - Express request object * @param {Object} req - Express request object
* @param {Object} res - Express response object * @param {Object} res - Express response object
*/ */
@ -385,6 +433,38 @@ async function updateResponseStatus(req, res) {
return res.status(400).json({ error: 'Invalid 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 }); const updated = await nocodbService.updateRepresentativeResponse(id, { status });
console.log('Updated response:', JSON.stringify(updated, null, 2)); console.log('Updated response:', JSON.stringify(updated, null, 2));

View File

@ -687,6 +687,81 @@
background: white; background: white;
} }
/* Response moderation styles */
.response-card {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.response-card.pending {
border-left: 4px solid #f39c12;
}
.response-card.approved {
border-left: 4px solid #27ae60;
}
.response-card.rejected {
border-left: 4px solid #e74c3c;
}
.response-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.response-meta {
color: #666;
font-size: 0.9rem;
margin-bottom: 1rem;
}
.response-meta p {
margin: 0.25rem 0;
}
.response-content {
background: #f8f9fa;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
.response-content h4 {
margin: 0 0 0.5rem 0;
color: #2c3e50;
font-size: 1rem;
}
.response-content p {
margin: 0;
color: #444;
line-height: 1.5;
}
.response-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.response-screenshot {
max-width: 200px;
margin-top: 0.5rem;
border-radius: 4px;
cursor: pointer;
}
.response-screenshot:hover {
opacity: 0.8;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.admin-nav { .admin-nav {
flex-direction: column; flex-direction: column;
@ -994,14 +1069,25 @@ Sincerely,
<!-- Response Moderation Tab --> <!-- Response Moderation Tab -->
<div id="responses-tab" class="tab-content"> <div id="responses-tab" class="tab-content">
<div class="form-row" style="align-items: center; margin-bottom: 2rem;"> <h2>Response Moderation</h2>
<h2 style="margin: 0;">Response Moderation</h2>
<select id="admin-response-status" class="form-control" style="max-width: 200px; margin-left: auto;"> <div style="display: flex; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap;">
<option value="pending">Pending</option> <div class="campaign-selector" style="flex: 1; min-width: 300px; margin: 0;">
<option value="approved">Approved</option> <label for="admin-campaign-filter">Filter by Campaign:</label>
<option value="rejected">Rejected</option> <select id="admin-campaign-filter" style="width: 100%; padding: 0.75rem; border: 2px solid #e0e6ed; border-radius: 8px;">
<option value="all">All</option> <option value="">All Campaigns</option>
</select> </select>
</div>
<div class="campaign-selector" style="flex: 1; min-width: 200px; margin: 0;">
<label for="admin-response-status">Filter by Status:</label>
<select id="admin-response-status" style="width: 100%; padding: 0.75rem; border: 2px solid #e0e6ed; border-radius: 8px;">
<option value="all">All Statuses</option>
<option value="pending" selected>Pending</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
</select>
</div>
</div> </div>
<div id="responses-loading" class="loading hidden"> <div id="responses-loading" class="loading hidden">

View File

@ -6,25 +6,33 @@
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
padding: 2rem; padding: 2rem;
border-radius: 8px; border-radius: 12px;
margin-bottom: 2rem; margin-bottom: 2rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
} }
.stat-item { .stat-item {
text-align: center; text-align: center;
padding: 0 1rem;
} }
.stat-number { .stat-number {
display: block; display: block;
font-size: 2.5rem; font-size: 3rem;
font-weight: bold; font-weight: bold;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
line-height: 1;
} }
.stat-label { .stat-label {
display: block; display: block;
font-size: 0.9rem; font-size: 1rem;
opacity: 0.9; color: #ffffff;
opacity: 1;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
} }
.response-controls { .response-controls {

View File

@ -510,6 +510,81 @@
background: white; background: white;
} }
/* Response moderation styles */
.response-card {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.response-card.pending {
border-left: 4px solid #f39c12;
}
.response-card.approved {
border-left: 4px solid #27ae60;
}
.response-card.rejected {
border-left: 4px solid #e74c3c;
}
.response-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.response-meta {
color: #666;
font-size: 0.9rem;
margin-bottom: 1rem;
}
.response-meta p {
margin: 0.25rem 0;
}
.response-content {
background: #f8f9fa;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
.response-content h4 {
margin: 0 0 0.5rem 0;
color: #2c3e50;
font-size: 1rem;
}
.response-content p {
margin: 0;
color: #444;
line-height: 1.5;
}
.response-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.response-screenshot {
max-width: 200px;
margin-top: 0.5rem;
border-radius: 4px;
cursor: pointer;
}
.response-screenshot:hover {
opacity: 0.8;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.dashboard-nav { .dashboard-nav {
flex-direction: column; flex-direction: column;
@ -557,6 +632,7 @@
<button class="nav-btn active" data-tab="campaigns">My Campaigns</button> <button class="nav-btn active" data-tab="campaigns">My Campaigns</button>
<button class="nav-btn" data-tab="create">Create Campaign</button> <button class="nav-btn" data-tab="create">Create Campaign</button>
<button class="nav-btn" data-tab="edit">Edit Campaign</button> <button class="nav-btn" data-tab="edit">Edit Campaign</button>
<button class="nav-btn" data-tab="responses">Responses</button>
<button class="nav-btn" data-tab="analytics">Analytics</button> <button class="nav-btn" data-tab="analytics">Analytics</button>
<button class="nav-btn" data-tab="account">Account</button> <button class="nav-btn" data-tab="account">Account</button>
</nav> </nav>
@ -810,6 +886,7 @@ Sincerely,
</form> </form>
</div> </div>
<!-- Analytics Tab -->
<!-- Analytics Tab --> <!-- Analytics Tab -->
<div id="analytics-tab" class="tab-content"> <div id="analytics-tab" class="tab-content">
<h2>Your Campaign Analytics</h2> <h2>Your Campaign Analytics</h2>
@ -841,6 +918,39 @@ Sincerely,
</div> </div>
</div> </div>
<!-- Responses Tab -->
<div id="responses-tab" class="tab-content">
<h2>Moderate Responses</h2>
<div style="display: flex; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap;">
<div class="campaign-selector" style="flex: 1; min-width: 300px; margin: 0;">
<label for="responses-campaign-filter">Filter by Campaign:</label>
<select id="responses-campaign-filter" style="width: 100%; padding: 0.75rem; border: 2px solid #e0e6ed; border-radius: 8px;">
<option value="">All My Campaigns</option>
</select>
</div>
<div class="campaign-selector" style="flex: 1; min-width: 200px; margin: 0;">
<label for="responses-status-filter">Filter by Status:</label>
<select id="responses-status-filter" style="width: 100%; padding: 0.75rem; border: 2px solid #e0e6ed; border-radius: 8px;">
<option value="all">All Statuses</option>
<option value="pending" selected>Pending</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
</select>
</div>
</div>
<div id="responses-loading" class="loading hidden">
<div class="spinner"></div>
<p>Loading responses...</p>
</div>
<div id="responses-list">
<!-- Responses will be loaded here -->
</div>
</div>
<!-- Account Tab --> <!-- Account Tab -->
<div id="account-tab" class="tab-content"> <div id="account-tab" class="tab-content">
<h2>Account Settings</h2> <h2>Account Settings</h2>

View File

@ -117,6 +117,14 @@ class AdminPanel {
this.loadAdminResponses(); this.loadAdminResponses();
}); });
} }
// Response campaign filter
const responseCampaignFilter = document.getElementById('admin-campaign-filter');
if (responseCampaignFilter) {
responseCampaignFilter.addEventListener('change', () => {
this.loadAdminResponses();
});
}
} }
setupFormInteractions() { setupFormInteractions() {
@ -1065,14 +1073,23 @@ class AdminPanel {
// Response Moderation Functions // Response Moderation Functions
async loadAdminResponses() { async loadAdminResponses() {
const status = document.getElementById('admin-response-status').value; const status = document.getElementById('admin-response-status').value;
const campaignSlug = document.getElementById('admin-campaign-filter')?.value || '';
const container = document.getElementById('admin-responses-container'); const container = document.getElementById('admin-responses-container');
const loading = document.getElementById('responses-loading'); const loading = document.getElementById('responses-loading');
// Populate campaign filter if not already done
await this.populateAdminCampaignFilter();
loading.classList.remove('hidden'); loading.classList.remove('hidden');
container.innerHTML = ''; container.innerHTML = '';
try { try {
const params = new URLSearchParams({ status, limit: 100 }); const params = new URLSearchParams({ status, limit: 100 });
if (campaignSlug) {
params.append('campaign_slug', campaignSlug);
}
const response = await window.apiClient.get(`/admin/responses?${params}`); const response = await window.apiClient.get(`/admin/responses?${params}`);
loading.classList.add('hidden'); loading.classList.add('hidden');
@ -1089,6 +1106,29 @@ class AdminPanel {
} }
} }
async populateAdminCampaignFilter() {
const filterSelect = document.getElementById('admin-campaign-filter');
if (!filterSelect || filterSelect.dataset.populated === 'true') return;
// Use already loaded campaigns or fetch them
if (this.campaigns.length === 0) {
await this.loadCampaigns();
}
// Clear existing options except the first one
filterSelect.innerHTML = '<option value="">All Campaigns</option>';
// Add campaign options
this.campaigns.forEach(campaign => {
const option = document.createElement('option');
option.value = campaign.slug;
option.textContent = campaign.title;
filterSelect.appendChild(option);
});
filterSelect.dataset.populated = 'true';
}
renderAdminResponses(responses) { renderAdminResponses(responses) {
const container = document.getElementById('admin-responses-container'); const container = document.getElementById('admin-responses-container');

View File

@ -121,6 +121,22 @@ class UserDashboard {
}); });
} }
// Response filter changes
const responseCampaignFilter = document.getElementById('responses-campaign-filter');
const responseStatusFilter = document.getElementById('responses-status-filter');
if (responseCampaignFilter) {
responseCampaignFilter.addEventListener('change', () => {
this.loadResponses();
});
}
if (responseStatusFilter) {
responseStatusFilter.addEventListener('change', () => {
this.loadResponses();
});
}
// Campaign actions using event delegation // Campaign actions using event delegation
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
if (e.target.matches('[data-action="view-campaign"]')) { if (e.target.matches('[data-action="view-campaign"]')) {
@ -141,6 +157,16 @@ class UserDashboard {
if (e.target.matches('[data-action="go-to-create"]')) { if (e.target.matches('[data-action="go-to-create"]')) {
this.switchTab('create'); this.switchTab('create');
} }
// Response moderation actions
if (e.target.matches('[data-action="approve-response"]')) {
this.moderateResponse(e.target.dataset.responseId, 'approved');
}
if (e.target.matches('[data-action="reject-response"]')) {
this.moderateResponse(e.target.dataset.responseId, 'rejected');
}
if (e.target.matches('[data-action="pending-response"]')) {
this.moderateResponse(e.target.dataset.responseId, 'pending');
}
}); });
// Setup campaign selector dropdowns // Setup campaign selector dropdowns
@ -374,6 +400,8 @@ class UserDashboard {
this.loadUserCampaigns(); this.loadUserCampaigns();
} else if (tabName === 'analytics') { } else if (tabName === 'analytics') {
this.loadAnalytics(); this.loadAnalytics();
} else if (tabName === 'responses') {
this.loadResponses();
} }
} }
@ -1015,6 +1043,183 @@ class UserDashboard {
} }
} }
// Response Moderation Methods
async loadResponses() {
const loadingDiv = document.getElementById('responses-loading');
const listDiv = document.getElementById('responses-list');
if (loadingDiv) loadingDiv.classList.remove('hidden');
try {
// Populate campaign filter dropdown if not already done
await this.populateResponseCampaignFilter();
// Get filter values
const campaignSlug = document.getElementById('responses-campaign-filter')?.value || '';
const status = document.getElementById('responses-status-filter')?.value || 'all';
// Build query params
const params = new URLSearchParams({
status: status,
limit: 100
});
if (campaignSlug) {
params.append('campaign_slug', campaignSlug);
}
const response = await window.apiClient.get(`/admin/responses?${params.toString()}`);
if (response.success) {
this.renderResponsesList(response.responses || []);
} else {
throw new Error(response.error || 'Failed to load responses');
}
} catch (error) {
console.error('Load responses error:', error);
if (listDiv) {
listDiv.innerHTML = `
<div class="empty-state">
<h3>Error loading responses</h3>
<p>${this.escapeHtml(error.message)}</p>
</div>
`;
}
} finally {
if (loadingDiv) loadingDiv.classList.add('hidden');
}
}
async populateResponseCampaignFilter() {
const filterSelect = document.getElementById('responses-campaign-filter');
if (!filterSelect || filterSelect.dataset.populated === 'true') return;
// Use already loaded campaigns
if (this.campaigns.length === 0) {
await this.loadUserCampaigns();
}
// Clear existing options except the first one
filterSelect.innerHTML = '<option value="">All My Campaigns</option>';
// Add campaign options
this.campaigns.forEach(campaign => {
const option = document.createElement('option');
option.value = campaign.slug;
option.textContent = campaign.title;
filterSelect.appendChild(option);
});
filterSelect.dataset.populated = 'true';
}
renderResponsesList(responses) {
const listDiv = document.getElementById('responses-list');
if (!listDiv) return;
if (responses.length === 0) {
listDiv.innerHTML = `
<div class="empty-state">
<h3>No responses found</h3>
<p>There are no responses matching your filters.</p>
</div>
`;
return;
}
listDiv.innerHTML = responses.map(response => this.renderResponseCard(response)).join('');
}
renderResponseCard(response) {
const statusLabel = {
pending: '⏳ Pending',
approved: '✅ Approved',
rejected: '❌ Rejected'
}[response.status] || response.status;
const levelEmoji = {
'Federal': '🍁',
'Provincial': '🏛️',
'Municipal': '🏙️',
'School Board': '🎓'
}[response.representative_level] || '📍';
// Show appropriate action buttons based on status
let actionButtons = '';
if (response.status === 'pending') {
actionButtons = `
<button class="btn btn-success" data-action="approve-response" data-response-id="${response.id}"> Approve</button>
<button class="btn btn-danger" data-action="reject-response" data-response-id="${response.id}"> Reject</button>
`;
} else if (response.status === 'approved') {
actionButtons = `
<button class="btn btn-secondary" data-action="pending-response" data-response-id="${response.id}"> Move to Pending</button>
<button class="btn btn-danger" data-action="reject-response" data-response-id="${response.id}"> Reject</button>
`;
} else if (response.status === 'rejected') {
actionButtons = `
<button class="btn btn-success" data-action="approve-response" data-response-id="${response.id}"> Approve</button>
<button class="btn btn-secondary" data-action="pending-response" data-response-id="${response.id}"> Move to Pending</button>
`;
}
return `
<div class="response-card ${response.status}" data-response-id="${response.id}">
<div class="response-header">
<div>
<span class="status-badge status-${response.status}">${statusLabel}</span>
</div>
<small style="color: #666;">Submitted ${this.formatDate(response.created_at)}</small>
</div>
<div class="response-meta">
<p><strong>Campaign:</strong> ${this.escapeHtml(response.campaign_slug)}</p>
<p><strong>${levelEmoji} Representative:</strong> ${this.escapeHtml(response.representative_name)}</p>
${response.representative_title ? `<p><strong>Title:</strong> ${this.escapeHtml(response.representative_title)}</p>` : ''}
<p><strong>Response Type:</strong> ${this.escapeHtml(response.response_type)}</p>
${response.submitted_by_name ? `<p><strong>Submitted By:</strong> ${this.escapeHtml(response.submitted_by_name)}</p>` : ''}
${response.submitted_by_email ? `<p><strong>Email:</strong> ${this.escapeHtml(response.submitted_by_email)}</p>` : ''}
</div>
<div class="response-content">
<h4>Response:</h4>
<p>${this.escapeHtml(response.response_text)}</p>
${response.user_comment ? `
<h4 style="margin-top: 1rem;">Submitter Comment:</h4>
<p>${this.escapeHtml(response.user_comment)}</p>
` : ''}
${response.screenshot_url ? `
<img src="${response.screenshot_url}" alt="Response screenshot" class="response-screenshot">
` : ''}
</div>
<div class="response-actions">
${actionButtons}
<span style="margin-left: auto; color: #666;">👍 ${response.upvote_count || 0} upvotes</span>
</div>
</div>
`;
}
async moderateResponse(responseId, newStatus) {
try {
const response = await window.apiClient.patch(`/admin/responses/${responseId}/status`, {
status: newStatus
});
if (response.success) {
this.showMessage(`Response ${newStatus} successfully!`, 'success');
// Reload responses to update the display
await this.loadResponses();
} else {
throw new Error(response.error || 'Failed to update response status');
}
} catch (error) {
console.error('Moderate response error:', error);
this.showMessage('Failed to moderate response: ' + error.message, 'error');
}
}
// Campaign Creation Methods // Campaign Creation Methods
async handleCreateCampaign(e) { async handleCreateCampaign(e) {
e.preventDefault(); e.preventDefault();

View File

@ -214,9 +214,9 @@ router.post(
router.post('/responses/:id/upvote', optionalAuth, rateLimiter.general, responsesController.upvoteResponse); router.post('/responses/:id/upvote', optionalAuth, rateLimiter.general, responsesController.upvoteResponse);
router.delete('/responses/:id/upvote', optionalAuth, rateLimiter.general, responsesController.removeUpvote); router.delete('/responses/:id/upvote', optionalAuth, rateLimiter.general, responsesController.removeUpvote);
// Admin Response Management Routes // Admin and Campaign Owner Response Management Routes
router.get('/admin/responses', requireAdmin, rateLimiter.general, responsesController.getAdminResponses); router.get('/admin/responses', requireNonTemp, rateLimiter.general, responsesController.getAdminResponses);
router.patch('/admin/responses/:id/status', requireAdmin, rateLimiter.general, router.patch('/admin/responses/:id/status', requireNonTemp, rateLimiter.general,
[body('status').isIn(['pending', 'approved', 'rejected']).withMessage('Invalid status')], [body('status').isIn(['pending', 'approved', 'rejected']).withMessage('Invalid status')],
handleValidationErrors, handleValidationErrors,
responsesController.updateResponseStatus responsesController.updateResponseStatus