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