const db = require('../models/db'); const logger = require('../utils/logger'); // Get all resources with optional filters async function getResources(req, res) { try { const { cities, // Multi-select: comma-separated city names types, // Multi-select: comma-separated resource types contact, // Multi-select: comma-separated contact methods (phone, email, website) page = 1, limit = 50, sort = 'name', order = 'asc' } = req.query; const offset = (page - 1) * limit; const params = []; let whereClause = 'WHERE is_active = true'; // Multi-select city filter if (cities) { const cityList = cities.split(',').map(c => c.trim()).filter(c => c); if (cityList.length > 0) { const placeholders = cityList.map((_, i) => `LOWER($${params.length + i + 1})`).join(', '); params.push(...cityList); whereClause += ` AND LOWER(city) IN (${placeholders})`; } } // Multi-select type filter if (types) { const typeList = types.split(',').map(t => t.trim()).filter(t => t); if (typeList.length > 0) { const placeholders = typeList.map((_, i) => `$${params.length + i + 1}`).join(', '); params.push(...typeList); whereClause += ` AND resource_type IN (${placeholders})`; } } // Multi-select contact filter if (contact) { const contactList = contact.split(',').map(c => c.trim()).filter(c => c); const contactConditions = []; if (contactList.includes('phone')) contactConditions.push('phone IS NOT NULL AND phone != \'\''); if (contactList.includes('email')) contactConditions.push('email IS NOT NULL AND email != \'\''); if (contactList.includes('website')) contactConditions.push('website IS NOT NULL AND website != \'\''); if (contactConditions.length > 0) { whereClause += ` AND (${contactConditions.join(' OR ')})`; } } // Validate sort column const validSorts = ['name', 'city', 'resource_type', 'updated_at']; const sortColumn = validSorts.includes(sort) ? sort : 'name'; const sortOrder = order.toLowerCase() === 'desc' ? 'DESC' : 'ASC'; // Get total count const countResult = await db.query( `SELECT COUNT(*) FROM food_resources ${whereClause}`, params ); const total = parseInt(countResult.rows[0].count); // Get resources params.push(limit, offset); const result = await db.query(` SELECT id, name, description, resource_type, address, city, province, postal_code, latitude, longitude, phone, email, website, hours_of_operation, eligibility, services_offered, source, source_url, updated_at, last_verified_at FROM food_resources ${whereClause} ORDER BY ${sortColumn} ${sortOrder} LIMIT $${params.length - 1} OFFSET $${params.length} `, params); res.json({ resources: result.rows, pagination: { page: parseInt(page), limit: parseInt(limit), total, pages: Math.ceil(total / limit) } }); } catch (error) { logger.error('Failed to get resources', { error: error.message }); res.status(500).json({ error: 'Failed to fetch resources' }); } } // Search resources by text async function searchResources(req, res) { try { const { q, cities, types, contact, limit = 50 } = req.query; if (!q || q.length < 2) { return res.status(400).json({ error: 'Search query must be at least 2 characters' }); } const params = [`%${q}%`]; let whereClause = ` WHERE is_active = true AND ( LOWER(name) LIKE LOWER($1) OR LOWER(description) LIKE LOWER($1) OR LOWER(address) LIKE LOWER($1) OR LOWER(services_offered) LIKE LOWER($1) ) `; // Multi-select city filter if (cities) { const cityList = cities.split(',').map(c => c.trim()).filter(c => c); if (cityList.length > 0) { const placeholders = cityList.map((_, i) => `LOWER($${params.length + i + 1})`).join(', '); params.push(...cityList); whereClause += ` AND LOWER(city) IN (${placeholders})`; } } // Multi-select type filter if (types) { const typeList = types.split(',').map(t => t.trim()).filter(t => t); if (typeList.length > 0) { const placeholders = typeList.map((_, i) => `$${params.length + i + 1}`).join(', '); params.push(...typeList); whereClause += ` AND resource_type IN (${placeholders})`; } } // Multi-select contact filter if (contact) { const contactList = contact.split(',').map(c => c.trim()).filter(c => c); const contactConditions = []; if (contactList.includes('phone')) contactConditions.push('phone IS NOT NULL AND phone != \'\''); if (contactList.includes('email')) contactConditions.push('email IS NOT NULL AND email != \'\''); if (contactList.includes('website')) contactConditions.push('website IS NOT NULL AND website != \'\''); if (contactConditions.length > 0) { whereClause += ` AND (${contactConditions.join(' OR ')})`; } } params.push(limit); const result = await db.query(` SELECT id, name, description, resource_type, address, city, phone, website, hours_of_operation, latitude, longitude FROM food_resources ${whereClause} ORDER BY CASE WHEN LOWER(name) LIKE LOWER($1) THEN 0 ELSE 1 END, name LIMIT $${params.length} `, params); res.json({ resources: result.rows }); } catch (error) { logger.error('Search failed', { error: error.message }); res.status(500).json({ error: 'Search failed' }); } } // Get nearby resources async function getNearbyResources(req, res) { try { const { lat, lng, radius = 25, limit = 20 } = req.query; if (!lat || !lng) { return res.status(400).json({ error: 'Latitude and longitude required' }); } const latitude = parseFloat(lat); const longitude = parseFloat(lng); const radiusKm = parseFloat(radius); // Haversine formula for distance calculation const result = await db.query(` SELECT id, name, description, resource_type, address, city, phone, website, hours_of_operation, latitude, longitude, (6371 * acos( cos(radians($1)) * cos(radians(latitude)) * cos(radians(longitude) - radians($2)) + sin(radians($1)) * sin(radians(latitude)) )) AS distance_km FROM food_resources WHERE is_active = true AND latitude IS NOT NULL AND longitude IS NOT NULL AND (6371 * acos( cos(radians($1)) * cos(radians(latitude)) * cos(radians(longitude) - radians($2)) + sin(radians($1)) * sin(radians(latitude)) )) < $3 ORDER BY distance_km LIMIT $4 `, [latitude, longitude, radiusKm, limit]); res.json({ resources: result.rows }); } catch (error) { logger.error('Nearby search failed', { error: error.message }); res.status(500).json({ error: 'Nearby search failed' }); } } // Get single resource by ID async function getResourceById(req, res) { try { const { id } = req.params; const result = await db.query(` SELECT * FROM food_resources WHERE id = $1 AND is_active = true `, [id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Resource not found' }); } res.json({ resource: result.rows[0] }); } catch (error) { logger.error('Failed to get resource', { error: error.message, id: req.params.id }); res.status(500).json({ error: 'Failed to fetch resource' }); } } // Get list of cities async function getCities(req, res) { try { const result = await db.query(` SELECT DISTINCT city, COUNT(*) as count FROM food_resources WHERE is_active = true AND city IS NOT NULL GROUP BY city ORDER BY count DESC, city `); res.json({ cities: result.rows }); } catch (error) { logger.error('Failed to get cities', { error: error.message }); res.status(500).json({ error: 'Failed to fetch cities' }); } } // Get list of resource types async function getTypes(req, res) { try { const result = await db.query(` SELECT resource_type, COUNT(*) as count FROM food_resources WHERE is_active = true GROUP BY resource_type ORDER BY count DESC `); // Map enum values to friendly names const typeNames = { 'food_bank': 'Food Bank', 'community_meal': 'Community Meal', 'hamper': 'Food Hamper', 'pantry': 'Food Pantry', 'soup_kitchen': 'Soup Kitchen', 'mobile_food': 'Mobile Food', 'grocery_program': 'Grocery Program', 'other': 'Other' }; const types = result.rows.map(row => ({ value: row.resource_type, label: typeNames[row.resource_type] || row.resource_type, count: parseInt(row.count) })); res.json({ types }); } catch (error) { logger.error('Failed to get types', { error: error.message }); res.status(500).json({ error: 'Failed to fetch types' }); } } // Get all resources for map display (minimal data, no pagination) async function getAllResourcesForMap(req, res) { try { const { cities, types, contact } = req.query; const params = []; let whereClause = 'WHERE is_active = true AND latitude IS NOT NULL AND longitude IS NOT NULL'; // Multi-select city filter if (cities) { const cityList = cities.split(',').map(c => c.trim()).filter(c => c); if (cityList.length > 0) { const placeholders = cityList.map((_, i) => `LOWER($${params.length + i + 1})`).join(', '); params.push(...cityList); whereClause += ` AND LOWER(city) IN (${placeholders})`; } } // Multi-select type filter if (types) { const typeList = types.split(',').map(t => t.trim()).filter(t => t); if (typeList.length > 0) { const placeholders = typeList.map((_, i) => `$${params.length + i + 1}`).join(', '); params.push(...typeList); whereClause += ` AND resource_type IN (${placeholders})`; } } // Multi-select contact filter if (contact) { const contactList = contact.split(',').map(c => c.trim()).filter(c => c); const contactConditions = []; if (contactList.includes('phone')) contactConditions.push('phone IS NOT NULL AND phone != \'\''); if (contactList.includes('email')) contactConditions.push('email IS NOT NULL AND email != \'\''); if (contactList.includes('website')) contactConditions.push('website IS NOT NULL AND website != \'\''); if (contactConditions.length > 0) { whereClause += ` AND (${contactConditions.join(' OR ')})`; } } const result = await db.query(` SELECT id, name, resource_type, address, city, latitude, longitude, geocode_confidence, geocode_provider FROM food_resources ${whereClause} ORDER BY name `, params); res.json({ resources: result.rows }); } catch (error) { logger.error('Failed to get map resources', { error: error.message }); res.status(500).json({ error: 'Failed to fetch map resources' }); } } // Get statistics async function getStats(req, res) { try { const result = await db.query(` SELECT COUNT(*) as total_resources, COUNT(DISTINCT city) as total_cities, COUNT(CASE WHEN resource_type = 'food_bank' THEN 1 END) as food_banks, COUNT(CASE WHEN resource_type = 'community_meal' THEN 1 END) as community_meals, MAX(updated_at) as last_updated FROM food_resources WHERE is_active = true `); res.json({ stats: result.rows[0] }); } catch (error) { logger.error('Failed to get stats', { error: error.message }); res.status(500).json({ error: 'Failed to fetch stats' }); } } module.exports = { getResources, searchResources, getNearbyResources, getResourceById, getCities, getTypes, getStats, getAllResourcesForMap };