freealberta/freealberta-food/app/controllers/updateRequestController.js

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
};