571 lines
16 KiB
JavaScript
571 lines
16 KiB
JavaScript
const db = require('../models/db');
|
|
const logger = require('../utils/logger');
|
|
|
|
// Submit an update request for a resource
|
|
async function submitUpdateRequest(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const {
|
|
submitter_email,
|
|
submitter_name,
|
|
proposed_name,
|
|
proposed_description,
|
|
proposed_resource_type,
|
|
proposed_address,
|
|
proposed_city,
|
|
proposed_phone,
|
|
proposed_email,
|
|
proposed_website,
|
|
proposed_hours_of_operation,
|
|
proposed_eligibility,
|
|
proposed_services_offered,
|
|
additional_notes
|
|
} = req.body;
|
|
|
|
// Validate required fields
|
|
if (!submitter_email) {
|
|
return res.status(400).json({ error: 'Email is required' });
|
|
}
|
|
|
|
// Validate email format
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(submitter_email)) {
|
|
return res.status(400).json({ error: 'Invalid email format' });
|
|
}
|
|
|
|
// Check if resource exists
|
|
const resourceCheck = await db.query(
|
|
'SELECT id FROM food_resources WHERE id = $1 AND is_active = true',
|
|
[id]
|
|
);
|
|
|
|
if (resourceCheck.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Resource not found' });
|
|
}
|
|
|
|
// Insert update request
|
|
const result = await db.query(`
|
|
INSERT INTO listing_update_requests (
|
|
resource_id, submitter_email, submitter_name,
|
|
proposed_name, proposed_description, proposed_resource_type,
|
|
proposed_address, proposed_city, proposed_phone,
|
|
proposed_email, proposed_website, proposed_hours_of_operation,
|
|
proposed_eligibility, proposed_services_offered, additional_notes
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
|
RETURNING id, created_at
|
|
`, [
|
|
id,
|
|
submitter_email,
|
|
submitter_name || null,
|
|
proposed_name || null,
|
|
proposed_description || null,
|
|
proposed_resource_type || null,
|
|
proposed_address || null,
|
|
proposed_city || null,
|
|
proposed_phone || null,
|
|
proposed_email || null,
|
|
proposed_website || null,
|
|
proposed_hours_of_operation || null,
|
|
proposed_eligibility || null,
|
|
proposed_services_offered || null,
|
|
additional_notes || null
|
|
]);
|
|
|
|
logger.info('Update request submitted', {
|
|
requestId: result.rows[0].id,
|
|
resourceId: id,
|
|
submitterEmail: submitter_email
|
|
});
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: 'Update request submitted successfully',
|
|
requestId: result.rows[0].id
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Failed to submit update request', { error: error.message });
|
|
res.status(500).json({ error: 'Failed to submit update request' });
|
|
}
|
|
}
|
|
|
|
// Get all update requests (admin)
|
|
async function getUpdateRequests(req, res) {
|
|
try {
|
|
const { status = 'pending', limit = 50, offset = 0 } = req.query;
|
|
|
|
const result = await db.query(`
|
|
SELECT
|
|
ur.*,
|
|
fr.name as current_name,
|
|
fr.description as current_description,
|
|
fr.resource_type as current_resource_type,
|
|
fr.address as current_address,
|
|
fr.city as current_city,
|
|
fr.phone as current_phone,
|
|
fr.email as current_email,
|
|
fr.website as current_website,
|
|
fr.hours_of_operation as current_hours_of_operation,
|
|
fr.eligibility as current_eligibility,
|
|
fr.services_offered as current_services_offered
|
|
FROM listing_update_requests ur
|
|
JOIN food_resources fr ON ur.resource_id = fr.id
|
|
WHERE ur.status = $1
|
|
ORDER BY ur.created_at DESC
|
|
LIMIT $2 OFFSET $3
|
|
`, [status, limit, offset]);
|
|
|
|
const countResult = await db.query(
|
|
'SELECT COUNT(*) FROM listing_update_requests WHERE status = $1',
|
|
[status]
|
|
);
|
|
|
|
res.json({
|
|
requests: result.rows,
|
|
total: parseInt(countResult.rows[0].count),
|
|
pagination: {
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Failed to get update requests', { error: error.message });
|
|
res.status(500).json({ error: 'Failed to fetch update requests' });
|
|
}
|
|
}
|
|
|
|
// Get counts of requests by status (admin)
|
|
async function getRequestCounts(req, res) {
|
|
try {
|
|
const result = await db.query(`
|
|
SELECT status, COUNT(*) as count
|
|
FROM listing_update_requests
|
|
GROUP BY status
|
|
`);
|
|
|
|
const counts = {
|
|
pending: 0,
|
|
approved: 0,
|
|
rejected: 0
|
|
};
|
|
|
|
result.rows.forEach(row => {
|
|
counts[row.status] = parseInt(row.count);
|
|
});
|
|
|
|
res.json({ counts });
|
|
|
|
} catch (error) {
|
|
logger.error('Failed to get request counts', { error: error.message });
|
|
res.status(500).json({ error: 'Failed to fetch request counts' });
|
|
}
|
|
}
|
|
|
|
// Approve an update request (admin)
|
|
async function approveRequest(req, res) {
|
|
const client = await db.getClient();
|
|
|
|
try {
|
|
const { id } = req.params;
|
|
const { admin_notes } = req.body;
|
|
|
|
await client.query('BEGIN');
|
|
|
|
// Get the update request
|
|
const requestResult = await client.query(
|
|
'SELECT * FROM listing_update_requests WHERE id = $1 AND status = $2',
|
|
[id, 'pending']
|
|
);
|
|
|
|
if (requestResult.rows.length === 0) {
|
|
await client.query('ROLLBACK');
|
|
return res.status(404).json({ error: 'Pending request not found' });
|
|
}
|
|
|
|
const request = requestResult.rows[0];
|
|
|
|
// Build the update query dynamically based on proposed changes
|
|
const updates = [];
|
|
const values = [];
|
|
let paramIndex = 1;
|
|
|
|
const fields = [
|
|
'name', 'description', 'resource_type', 'address', 'city',
|
|
'phone', 'email', 'website', 'hours_of_operation',
|
|
'eligibility', 'services_offered'
|
|
];
|
|
|
|
fields.forEach(field => {
|
|
const proposedValue = request[`proposed_${field}`];
|
|
if (proposedValue !== null) {
|
|
updates.push(`${field} = $${paramIndex}`);
|
|
values.push(proposedValue);
|
|
paramIndex++;
|
|
}
|
|
});
|
|
|
|
if (updates.length > 0) {
|
|
// Add updated_at
|
|
updates.push(`updated_at = CURRENT_TIMESTAMP`);
|
|
values.push(request.resource_id);
|
|
|
|
await client.query(`
|
|
UPDATE food_resources
|
|
SET ${updates.join(', ')}
|
|
WHERE id = $${paramIndex}
|
|
`, values);
|
|
}
|
|
|
|
// Mark request as approved
|
|
await client.query(`
|
|
UPDATE listing_update_requests
|
|
SET status = 'approved', admin_notes = $1, reviewed_at = CURRENT_TIMESTAMP, reviewed_by = 'admin'
|
|
WHERE id = $2
|
|
`, [admin_notes || null, id]);
|
|
|
|
await client.query('COMMIT');
|
|
|
|
logger.info('Update request approved', {
|
|
requestId: id,
|
|
resourceId: request.resource_id,
|
|
fieldsUpdated: updates.length
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Update request approved and changes applied',
|
|
fieldsUpdated: updates.length - 1 // Exclude updated_at
|
|
});
|
|
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
logger.error('Failed to approve update request', { error: error.message });
|
|
res.status(500).json({ error: 'Failed to approve update request' });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
// Reject an update request (admin)
|
|
async function rejectRequest(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { admin_notes } = req.body;
|
|
|
|
const result = await db.query(`
|
|
UPDATE listing_update_requests
|
|
SET status = 'rejected', admin_notes = $1, reviewed_at = CURRENT_TIMESTAMP, reviewed_by = 'admin'
|
|
WHERE id = $2 AND status = 'pending'
|
|
RETURNING id
|
|
`, [admin_notes || null, id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Pending request not found' });
|
|
}
|
|
|
|
logger.info('Update request rejected', { requestId: id });
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Update request rejected'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Failed to reject update request', { error: error.message });
|
|
res.status(500).json({ error: 'Failed to reject update request' });
|
|
}
|
|
}
|
|
|
|
// Validate admin password
|
|
async function validateAuth(req, res) {
|
|
const { password } = req.body;
|
|
const adminPassword = process.env.ADMIN_PASSWORD;
|
|
|
|
if (!adminPassword) {
|
|
logger.error('ADMIN_PASSWORD not configured');
|
|
return res.status(500).json({ error: 'Admin authentication not configured' });
|
|
}
|
|
|
|
if (password === adminPassword) {
|
|
res.json({ success: true });
|
|
} else {
|
|
res.status(401).json({ error: 'Invalid password' });
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// Listing Submissions (new listings)
|
|
// ==========================================
|
|
|
|
// Submit a new listing for approval
|
|
async function submitListingSubmission(req, res) {
|
|
try {
|
|
const {
|
|
submitter_email,
|
|
submitter_name,
|
|
name,
|
|
description,
|
|
resource_type,
|
|
address,
|
|
city,
|
|
phone,
|
|
email,
|
|
website,
|
|
hours_of_operation,
|
|
eligibility,
|
|
services_offered,
|
|
additional_notes
|
|
} = req.body;
|
|
|
|
// Validate required fields
|
|
if (!submitter_email) {
|
|
return res.status(400).json({ error: 'Your email is required' });
|
|
}
|
|
|
|
if (!name || !name.trim()) {
|
|
return res.status(400).json({ error: 'Listing name is required' });
|
|
}
|
|
|
|
// Validate email format
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(submitter_email)) {
|
|
return res.status(400).json({ error: 'Invalid email format' });
|
|
}
|
|
|
|
// Insert listing submission
|
|
const result = await db.query(`
|
|
INSERT INTO listing_submissions (
|
|
submitter_email, submitter_name,
|
|
name, description, resource_type,
|
|
address, city, phone, email, website,
|
|
hours_of_operation, eligibility, services_offered,
|
|
additional_notes
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
|
RETURNING id, created_at
|
|
`, [
|
|
submitter_email,
|
|
submitter_name || null,
|
|
name.trim(),
|
|
description || null,
|
|
resource_type || 'other',
|
|
address || null,
|
|
city || null,
|
|
phone || null,
|
|
email || null,
|
|
website || null,
|
|
hours_of_operation || null,
|
|
eligibility || null,
|
|
services_offered || null,
|
|
additional_notes || null
|
|
]);
|
|
|
|
logger.info('Listing submission created', {
|
|
submissionId: result.rows[0].id,
|
|
submitterEmail: submitter_email,
|
|
listingName: name
|
|
});
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: 'Listing submitted successfully. It will be reviewed by our team.',
|
|
submissionId: result.rows[0].id
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Failed to submit listing', { error: error.message });
|
|
res.status(500).json({ error: 'Failed to submit listing' });
|
|
}
|
|
}
|
|
|
|
// Get all listing submissions (admin)
|
|
async function getListingSubmissions(req, res) {
|
|
try {
|
|
const { status = 'pending', limit = 50, offset = 0 } = req.query;
|
|
|
|
const result = await db.query(`
|
|
SELECT *
|
|
FROM listing_submissions
|
|
WHERE status = $1
|
|
ORDER BY created_at DESC
|
|
LIMIT $2 OFFSET $3
|
|
`, [status, limit, offset]);
|
|
|
|
const countResult = await db.query(
|
|
'SELECT COUNT(*) FROM listing_submissions WHERE status = $1',
|
|
[status]
|
|
);
|
|
|
|
res.json({
|
|
submissions: result.rows,
|
|
total: parseInt(countResult.rows[0].count),
|
|
pagination: {
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Failed to get listing submissions', { error: error.message });
|
|
res.status(500).json({ error: 'Failed to fetch listing submissions' });
|
|
}
|
|
}
|
|
|
|
// Get counts of listing submissions by status (admin)
|
|
async function getListingSubmissionCounts(req, res) {
|
|
try {
|
|
const result = await db.query(`
|
|
SELECT status, COUNT(*) as count
|
|
FROM listing_submissions
|
|
GROUP BY status
|
|
`);
|
|
|
|
const counts = {
|
|
pending: 0,
|
|
approved: 0,
|
|
rejected: 0
|
|
};
|
|
|
|
result.rows.forEach(row => {
|
|
counts[row.status] = parseInt(row.count);
|
|
});
|
|
|
|
res.json({ counts });
|
|
|
|
} catch (error) {
|
|
logger.error('Failed to get listing submission counts', { error: error.message });
|
|
res.status(500).json({ error: 'Failed to fetch submission counts' });
|
|
}
|
|
}
|
|
|
|
// Approve a listing submission (admin) - creates the actual resource
|
|
async function approveListingSubmission(req, res) {
|
|
const client = await db.getClient();
|
|
|
|
try {
|
|
const { id } = req.params;
|
|
const { admin_notes } = req.body;
|
|
|
|
await client.query('BEGIN');
|
|
|
|
// Get the submission
|
|
const submissionResult = await client.query(
|
|
'SELECT * FROM listing_submissions WHERE id = $1 AND status = $2',
|
|
[id, 'pending']
|
|
);
|
|
|
|
if (submissionResult.rows.length === 0) {
|
|
await client.query('ROLLBACK');
|
|
return res.status(404).json({ error: 'Pending submission not found' });
|
|
}
|
|
|
|
const submission = submissionResult.rows[0];
|
|
|
|
// Create the new resource in food_resources
|
|
const resourceResult = await client.query(`
|
|
INSERT INTO food_resources (
|
|
name, description, resource_type,
|
|
address, city, phone, email, website,
|
|
hours_of_operation, eligibility, services_offered,
|
|
source, source_id
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'manual', $12)
|
|
RETURNING id
|
|
`, [
|
|
submission.name,
|
|
submission.description,
|
|
submission.resource_type,
|
|
submission.address,
|
|
submission.city,
|
|
submission.phone,
|
|
submission.email,
|
|
submission.website,
|
|
submission.hours_of_operation,
|
|
submission.eligibility,
|
|
submission.services_offered,
|
|
`submission_${submission.id}`
|
|
]);
|
|
|
|
const newResourceId = resourceResult.rows[0].id;
|
|
|
|
// Mark submission as approved and link to created resource
|
|
await client.query(`
|
|
UPDATE listing_submissions
|
|
SET status = 'approved',
|
|
admin_notes = $1,
|
|
reviewed_at = CURRENT_TIMESTAMP,
|
|
reviewed_by = 'admin',
|
|
created_resource_id = $2
|
|
WHERE id = $3
|
|
`, [admin_notes || null, newResourceId, id]);
|
|
|
|
await client.query('COMMIT');
|
|
|
|
logger.info('Listing submission approved', {
|
|
submissionId: id,
|
|
createdResourceId: newResourceId,
|
|
listingName: submission.name
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Listing approved and published',
|
|
resourceId: newResourceId
|
|
});
|
|
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
logger.error('Failed to approve listing submission', { error: error.message });
|
|
res.status(500).json({ error: 'Failed to approve submission' });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
// Reject a listing submission (admin)
|
|
async function rejectListingSubmission(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { admin_notes } = req.body;
|
|
|
|
const result = await db.query(`
|
|
UPDATE listing_submissions
|
|
SET status = 'rejected',
|
|
admin_notes = $1,
|
|
reviewed_at = CURRENT_TIMESTAMP,
|
|
reviewed_by = 'admin'
|
|
WHERE id = $2 AND status = 'pending'
|
|
RETURNING id
|
|
`, [admin_notes || null, id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Pending submission not found' });
|
|
}
|
|
|
|
logger.info('Listing submission rejected', { submissionId: id });
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Listing submission rejected'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Failed to reject listing submission', { error: error.message });
|
|
res.status(500).json({ error: 'Failed to reject submission' });
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
submitUpdateRequest,
|
|
getUpdateRequests,
|
|
getRequestCounts,
|
|
approveRequest,
|
|
rejectRequest,
|
|
validateAuth,
|
|
// Listing submissions
|
|
submitListingSubmission,
|
|
getListingSubmissions,
|
|
getListingSubmissionCounts,
|
|
approveListingSubmission,
|
|
rejectListingSubmission
|
|
};
|