const express = require('express'); const router = express.Router(); const moment = require('moment'); const DubaiRealEstateParser = require('../services/nlpService'); const SQLGenerator = require('../services/sqlGenerator'); const ContextAwareSQLGenerator = require('../services/contextAwareSQLGenerator'); const ChartFormatter = require('../services/chartFormatter'); const ContextManager = require('../services/contextManager'); const QueryTemplates = require('../services/queryTemplates'); const database = require('../models/database'); const { validateQuery } = require('../middleware/validation'); const nlpParser = new DubaiRealEstateParser(); const sqlGenerator = new SQLGenerator(); const contextAwareSQLGenerator = new ContextAwareSQLGenerator(); const chartFormatter = new ChartFormatter(); const contextManager = new ContextManager(); const templates = new QueryTemplates(); // Main query endpoint router.post('/query', validateQuery, async (req, res) => { try { const { query, sessionId } = req.validatedData; console.log(`🔍 Processing query: "${query}"`); // Step 1: Parse the natural language query const parsedQuery = await nlpParser.parseQuery(query); console.log('📝 Parsed query:', parsedQuery); // Step 2: Generate SQL query with context awareness const sqlQuery = await contextAwareSQLGenerator.generateSQL(parsedQuery, sessionId); console.log('🗄️ Generated SQL:', sqlQuery.sql); // Step 3: Execute SQL query const sqlResult = await database.query(sqlQuery.sql, sqlQuery.params); console.log(`📊 Query returned ${sqlResult.length} rows`); // Step 4: Format response for Chart.js // Use merged context if available (for follow-up queries) const contextForFormatter = sqlQuery.mergedContext || parsedQuery; const response = chartFormatter.formatResponse(contextForFormatter, sqlResult, sqlQuery); res.json(response); } catch (error) { console.error('❌ Query processing error:', error); res.status(500).json({ success: false, message: 'Failed to process query', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); // Specific query endpoints for the 10 questions // Q1-3: Rental price trend with context awareness router.post('/queries/rental-trend', async (req, res) => { try { const { area = 'business bay', sessionId, refinement } = req.body; const query = `Give me the last 6 months rental price trend for ${area}`; const parsedQuery = await nlpParser.parseQuery(query); // Apply refinement if provided if (refinement) { parsedQuery.refinements = contextManager.extractRefinements(refinement); } const sqlQuery = await contextAwareSQLGenerator.generateSQL(parsedQuery, sessionId); const sqlResult = await database.query(sqlQuery.sql, sqlQuery.params); const response = chartFormatter.formatResponse(parsedQuery, sqlResult, sqlQuery); res.json(response); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }); // Q4: Brief about the Project router.get('/queries/project-summary-detail', async (req, res) => { try { const params = { startDate: moment().subtract(6, 'months').format('YYYY-MM-DD') }; const sqlQuery = templates.getTemplate('project_transaction_summary', params); const sqlResult = await database.query(sqlQuery.sql, sqlQuery.params); res.json({ success: true, data: { text: 'Transaction summary for projects', visualizations: chartFormatter.createVisualizations(sqlResult, { intent: 'summary' }), cards: chartFormatter.createCards(sqlResult, { intent: 'summary' }) } }); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }); // Q5: Fast moving projects router.get('/queries/fast-moving-projects', async (req, res) => { try { const params = { startDate: moment().subtract(6, 'months').format('YYYY-MM-DD') }; const sqlQuery = templates.getTemplate('fast_moving_projects', params); const sqlResult = await database.query(sqlQuery.sql, sqlQuery.params); res.json({ success: true, data: { text: 'Fast moving projects in last 6 months', visualizations: chartFormatter.createVisualizations(sqlResult, { intent: 'compare' }), cards: chartFormatter.createCards(sqlResult, { intent: 'compare' }) } }); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }); // Q6: Off-plan uptick areas router.get('/queries/offplan-uptick', async (req, res) => { try { const params = { startDate: moment().subtract(6, 'months').format('YYYY-MM-DD') }; const sqlQuery = templates.getTemplate('offplan_uptick_areas', params); const sqlResult = await database.query(sqlQuery.sql, sqlQuery.params); res.json({ success: true, data: { text: 'Areas seeing uptick in off-plan projects', visualizations: chartFormatter.createVisualizations(sqlResult, { intent: 'compare' }), cards: chartFormatter.createCards(sqlResult, { intent: 'compare' }) } }); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }); // Predefined query endpoints for common questions router.get('/queries/rental-trend/:area?', async (req, res) => { try { const area = req.params.area || 'business bay'; const query = `Give me the last 6 months rental price trend for ${area}`; const parsedQuery = await nlpParser.parseQuery(query); const sqlQuery = sqlGenerator.generateSQL(parsedQuery); const sqlResult = await database.query(sqlQuery.sql, sqlQuery.params); const response = chartFormatter.formatResponse(parsedQuery, sqlResult, sqlQuery); res.json(response); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }); router.get('/queries/top-areas', async (req, res) => { try { const query = 'Which area is having more rental transactions?'; const parsedQuery = await nlpParser.parseQuery(query); const sqlQuery = sqlGenerator.generateSQL(parsedQuery); const sqlResult = await database.query(sqlQuery.sql, sqlQuery.params); const response = chartFormatter.formatResponse(parsedQuery, sqlResult, sqlQuery); res.json(response); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }); router.get('/queries/project-summary', async (req, res) => { try { const query = 'Brief about the Project'; const parsedQuery = await nlpParser.parseQuery(query); const sqlQuery = sqlGenerator.generateSQL(parsedQuery); const sqlResult = await database.query(sqlQuery.sql, sqlQuery.params); const response = chartFormatter.formatResponse(parsedQuery, sqlResult, sqlQuery); res.json(response); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }); router.get('/queries/commercial-leasing', async (req, res) => { try { const query = 'Top 5 areas for Commercial leasing and why?'; const parsedQuery = await nlpParser.parseQuery(query); const sqlQuery = sqlGenerator.generateSQL(parsedQuery); const sqlResult = await database.query(sqlQuery.sql, sqlQuery.params); const response = chartFormatter.formatResponse(parsedQuery, sqlResult, sqlQuery); res.json(response); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }); router.get('/queries/residential-leasing', async (req, res) => { try { const query = 'Top 5 areas for Residential leasing and why?'; const parsedQuery = await nlpParser.parseQuery(query); const sqlQuery = sqlGenerator.generateSQL(parsedQuery); const sqlResult = await database.query(sqlQuery.sql, sqlQuery.params); const response = chartFormatter.formatResponse(parsedQuery, sqlResult, sqlQuery); res.json(response); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }); // Database info endpoint router.get('/database/info', async (req, res) => { try { const tables = ['transactions', 'rents', 'projects', 'buildings', 'lands', 'valuations', 'brokers']; const info = {}; for (const table of tables) { try { const countResult = await database.query(`SELECT COUNT(*) as count FROM ${table}`); info[table] = { count: countResult[0].count, status: 'available' }; } catch (error) { info[table] = { count: 0, status: 'error', error: error.message }; } } res.json({ success: true, data: { database: process.env.DB_NAME || 'dubai_dld', tables: info, timestamp: new Date().toISOString() } }); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }); // Available queries endpoint router.get('/queries/available', (req, res) => { res.json({ success: true, data: { predefined_queries: [ { endpoint: '/api/queries/rental-trend/:area', description: 'Get rental price trend for a specific area', example: '/api/queries/rental-trend/business-bay' }, { endpoint: '/api/queries/top-areas', description: 'Get top areas with most rental transactions' }, { endpoint: '/api/queries/project-summary', description: 'Get project summary and statistics' }, { endpoint: '/api/queries/commercial-leasing', description: 'Get top areas for commercial leasing' }, { endpoint: '/api/queries/residential-leasing', description: 'Get top areas for residential leasing' } ], custom_queries: { endpoint: '/api/query', method: 'POST', description: 'Process custom natural language queries', examples: [ 'Give me the last 6 months rental price trend for Business Bay', 'Which area is having more rental transactions?', 'Top 5 areas for Commercial leasing and why?', 'Avg price of 3BHK apartment by area in last 6 months', 'Brief about the Project' ] } } }); }); // Recent transactions endpoint with filters router.get('/transactions/recent', async (req, res) => { try { const { area_name, property_type, size, size_min, size_max, beds, project, // pagination params page, page_size, limit } = req.query; // Validate limit only if provided; no upper cap enforced by API let limitNum = undefined; if (limit !== undefined && limit !== null && `${limit}`.trim() !== '') { const parsed = parseInt(limit, 10); if (isNaN(parsed) || parsed < 1) { return res.status(400).json({ success: false, message: 'Limit must be a positive integer when provided' }); } limitNum = parsed; } // Build base WHERE with optional filters let baseWhere = ' WHERE 1=1'; const params = []; // Apply filters (case-insensitive matching where applicable) if (area_name && area_name.trim() !== '') { baseWhere += ' AND LOWER(area_en) LIKE ?'; params.push(`%${area_name.toLowerCase().trim()}%`); } if (property_type && property_type.trim() !== '' && property_type !== 'all') { baseWhere += ' AND (LOWER(prop_type_en) LIKE ? OR LOWER(prop_sb_type_en) LIKE ?)'; const propType = `%${property_type.toLowerCase().trim()}%`; params.push(propType, propType); } // Size filtering: support size_min/size_max. Keep legacy 'size' as upper bound const minNum = size_min !== undefined ? parseFloat(size_min) : undefined; const maxNum = size_max !== undefined ? parseFloat(size_max) : (size !== undefined ? parseFloat(size) : undefined); const hasMin = minNum !== undefined && !isNaN(minNum); const hasMax = maxNum !== undefined && !isNaN(maxNum); if (hasMin && hasMax) { baseWhere += ' AND actual_area BETWEEN ? AND ?'; params.push(minNum, maxNum); } else if (hasMin) { baseWhere += ' AND actual_area >= ?'; params.push(minNum); } else if (hasMax) { baseWhere += ' AND actual_area <= ?'; params.push(maxNum); } if (beds && beds !== 'all' && beds !== '') { const bedsStr = beds.toString().trim(); // Match beds - handle formats like "3 b/r", "3.0", "3", "studio" if (bedsStr.toLowerCase() === 'studio') { baseWhere += ' AND LOWER(rooms_en) = ?'; params.push('studio'); } else if (bedsStr.toLowerCase() === 'null' || bedsStr === '') { baseWhere += ' AND (rooms_en IS NULL OR rooms_en = "")'; } else { baseWhere += ' AND (rooms_en LIKE ? OR rooms_en = ? OR rooms_en = ?)'; params.push(`%${bedsStr}%`, bedsStr, `${bedsStr}.0`); } } if (project && project.trim() !== '' && project !== 'all') { baseWhere += ' AND (LOWER(project_en) LIKE ? OR LOWER(master_project_en) LIKE ?)'; const projectName = `%${project.toLowerCase().trim()}%`; params.push(projectName, projectName); } // Pagination decision: page/page_size OR legacy limit triggers bounded fetch; otherwise full data const rawPageSize = page_size ?? undefined; const wantsPagination = (page !== undefined) || (rawPageSize !== undefined) || (limitNum !== undefined); const defaultPage = 1; const defaultPageSize = 30; const pageNum = (() => { const n = parseInt(page ?? defaultPage, 10); return isNaN(n) || n < 1 ? defaultPage : n; })(); const pageSizeNum = (() => { const n = parseInt((rawPageSize ?? (limitNum !== undefined ? limitNum : defaultPageSize)), 10); return isNaN(n) || n < 1 ? defaultPageSize : n; })(); const offsetNum = (pageNum - 1) * pageSizeNum; const orderBy = ' ORDER BY instance_date DESC, transaction_id DESC'; let dataSql; let total = null; if (!wantsPagination) { dataSql = ` SELECT * FROM transactions ${baseWhere} ${orderBy} `; } else if (limitNum !== undefined && page === undefined && page_size === undefined) { // Legacy behavior: only limit provided (no paging), apply LIMIT without OFFSET dataSql = ` SELECT * FROM transactions ${baseWhere} ${orderBy} LIMIT ${limitNum} `; } else { // Full pagination with COUNT const countSql = ` SELECT COUNT(*) AS total FROM transactions ${baseWhere} `; const countRows = await database.query(countSql, params); total = countRows && countRows[0] ? countRows[0].total : 0; dataSql = ` SELECT * FROM transactions ${baseWhere} ${orderBy} LIMIT ${pageSizeNum} OFFSET ${offsetNum} `; } console.log(`🔍 Fetching recent transactions with filters:`, { area_name, property_type, size, size_min, size_max, beds, project, limit: limitNum, page, page_size }); const transactions = await database.query(dataSql, params); const totalPages = (total !== null) ? Math.max(1, Math.ceil(total / pageSizeNum)) : 1; res.json({ success: true, data: { transactions: transactions, count: transactions.length, total: total !== null ? total : transactions.length, page: total !== null ? pageNum : null, page_size: total !== null ? pageSizeNum : (limitNum !== undefined ? limitNum : null), total_pages: total !== null ? totalPages : 1, has_next: total !== null ? pageNum < totalPages : false, has_prev: total !== null ? pageNum > 1 : false, filters: { area_name: area_name || null, property_type: property_type || null, size: size || null, beds: beds || null, project: project || null, limit: limitNum ?? null, page: total !== null ? pageNum : null, page_size: total !== null ? pageSizeNum : (limitNum !== undefined ? limitNum : null) } } }); } catch (error) { console.error('❌ Transactions query error:', error); res.status(500).json({ success: false, message: 'Failed to retrieve transactions', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); // Recent rents endpoint with filters router.get('/rents/recent', async (req, res) => { try { const { area_name, property_type, size_min, size_max, rooms, project, page, page_size, limit } = req.query; // Validate limit let limitNum = undefined; if (limit !== undefined && limit !== null && `${limit}`.trim() !== '') { const parsed = parseInt(limit, 10); if (isNaN(parsed) || parsed < 1) { return res.status(400).json({ success: false, message: 'Limit must be a positive integer when provided' }); } limitNum = parsed; } // Build WHERE clause let baseWhere = ' WHERE 1=1'; const params = []; // Area filter (case-insensitive) if (area_name && area_name.trim() !== '') { baseWhere += ' AND LOWER(area_en) LIKE ?'; params.push(`%${area_name.toLowerCase().trim()}%`); } // Property type filter if (property_type && property_type.trim() !== '' && property_type !== 'all') { baseWhere += ' AND (LOWER(prop_type_en) LIKE ? OR LOWER(prop_sub_type_en) LIKE ?)'; const propType = `%${property_type.toLowerCase().trim()}%`; params.push(propType, propType); } // Actual area filtering (min/max) const minNum = size_min !== undefined ? parseFloat(size_min) : undefined; const maxNum = size_max !== undefined ? parseFloat(size_max) : undefined; const hasMin = minNum !== undefined && !isNaN(minNum); const hasMax = maxNum !== undefined && !isNaN(maxNum); if (hasMin && hasMax) { baseWhere += ' AND actual_area BETWEEN ? AND ?'; params.push(minNum, maxNum); } else if (hasMin) { baseWhere += ' AND actual_area >= ?'; params.push(minNum); } else if (hasMax) { baseWhere += ' AND actual_area <= ?'; params.push(maxNum); } // Rooms filter (decimal values: 1.0, 2.0, etc.) if (rooms && rooms !== 'all' && rooms !== '') { const roomsNum = parseFloat(rooms); if (!isNaN(roomsNum)) { baseWhere += ' AND rooms = ?'; params.push(roomsNum); } } // Project filter if (project && project.trim() !== '' && project !== 'all') { baseWhere += ' AND (LOWER(project_en) LIKE ? OR LOWER(master_project_en) LIKE ?)'; const projectName = `%${project.toLowerCase().trim()}%`; params.push(projectName, projectName); } // Pagination logic const wantsPagination = (page !== undefined) || (page_size !== undefined) || (limitNum !== undefined); const defaultPage = 1; const defaultPageSize = 30; const pageNum = (() => { const n = parseInt(page ?? defaultPage, 10); return isNaN(n) || n < 1 ? defaultPage : n; })(); const pageSizeNum = (() => { const n = parseInt((page_size ?? (limitNum !== undefined ? limitNum : defaultPageSize)), 10); return isNaN(n) || n < 1 ? defaultPageSize : n; })(); const offsetNum = (pageNum - 1) * pageSizeNum; const orderBy = ' ORDER BY registration_date DESC, rent_id DESC'; let dataSql; let total = null; if (!wantsPagination) { dataSql = `SELECT * FROM rents ${baseWhere} ${orderBy}`; } else if (limitNum !== undefined && page === undefined && page_size === undefined) { dataSql = `SELECT * FROM rents ${baseWhere} ${orderBy} LIMIT ${limitNum}`; } else { const countSql = `SELECT COUNT(*) AS total FROM rents ${baseWhere}`; const countRows = await database.query(countSql, params); total = countRows && countRows[0] ? countRows[0].total : 0; dataSql = `SELECT * FROM rents ${baseWhere} ${orderBy} LIMIT ${pageSizeNum} OFFSET ${offsetNum}`; } console.log(`🔍 Fetching recent rents with filters:`, { area_name, property_type, size_min, size_max, rooms, project, limit: limitNum, page, page_size }); const rents = await database.query(dataSql, params); const totalPages = (total !== null) ? Math.max(1, Math.ceil(total / pageSizeNum)) : 1; res.json({ success: true, data: { rents: rents, count: rents.length, total: total !== null ? total : rents.length, page: total !== null ? pageNum : null, page_size: total !== null ? pageSizeNum : (limitNum !== undefined ? limitNum : null), total_pages: total !== null ? totalPages : 1, has_next: total !== null ? pageNum < totalPages : false, has_prev: total !== null ? pageNum > 1 : false, filters: { area_name: area_name || null, property_type: property_type || null, rooms: rooms || null, project: project || null, limit: limitNum ?? null, page: total !== null ? pageNum : null, page_size: total !== null ? pageSizeNum : (limitNum !== undefined ? limitNum : null) } } }); } catch (error) { console.error('❌ Rents query error:', error); res.status(500).json({ success: false, message: 'Failed to retrieve rents', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); // Unified endpoint for both rent and sale queries // Works exactly like /api/transactions/recent for type=sale // Works exactly like /api/rents/recent for type=rent router.get('/properties/recent', async (req, res) => { try { // Extract all possible parameters const { type, // Required: 'rent' or 'sale' area_name, property_type, size, // Legacy parameter for sale (upper bound) size_min, size_max, rooms, // For rent: decimal (2.0, 3.0). For sale: accepts string ("3", "studio", "3 b/r") beds, // Alternative parameter for sale (backward compatibility) project, page, page_size, limit } = req.query; // ============================================ // VALIDATION: Type parameter (required) // ============================================ if (!type || typeof type !== 'string') { return res.status(400).json({ success: false, message: 'Type parameter is required and must be either "rent" or "sale"' }); } const normalizedType = type.toLowerCase().trim(); if (normalizedType !== 'rent' && normalizedType !== 'sale') { return res.status(400).json({ success: false, message: 'Type parameter must be either "rent" or "sale"' }); } const isRent = normalizedType === 'rent'; // ============================================ // CONFIGURATION: Type-specific settings // ============================================ const config = { tableName: isRent ? 'rents' : 'transactions', dateColumn: isRent ? 'registration_date' : 'instance_date', idColumn: isRent ? 'rent_id' : 'transaction_id', dataKey: isRent ? 'rents' : 'transactions', propSubTypeColumn: isRent ? 'prop_sub_type_en' : 'prop_sb_type_en', roomsColumn: isRent ? 'rooms' : 'rooms_en' }; // ============================================ // VALIDATION: Limit parameter // ============================================ let limitNum = undefined; if (limit !== undefined && limit !== null && `${limit}`.trim() !== '') { const parsed = parseInt(limit, 10); if (isNaN(parsed) || parsed < 1) { return res.status(400).json({ success: false, message: 'Limit must be a positive integer when provided' }); } limitNum = parsed; } // ============================================ // BUILD WHERE CLAUSE: Base condition // ============================================ let baseWhere = ' WHERE 1=1'; const params = []; // ============================================ // FILTER: Area name (case-insensitive, partial match) // ============================================ if (area_name !== undefined && area_name !== null && typeof area_name === 'string' && area_name.trim() !== '') { baseWhere += ' AND LOWER(area_en) LIKE ?'; params.push(`%${area_name.toLowerCase().trim()}%`); } // ============================================ // FILTER: Property type (case-insensitive, partial match) // ============================================ if (property_type !== undefined && property_type !== null && typeof property_type === 'string' && property_type.trim() !== '' && property_type.toLowerCase().trim() !== 'all') { baseWhere += ` AND (LOWER(prop_type_en) LIKE ? OR LOWER(${config.propSubTypeColumn}) LIKE ?)`; const propType = `%${property_type.toLowerCase().trim()}%`; params.push(propType, propType); } // ============================================ // FILTER: Size (area) - Different logic for rent vs sale // ============================================ if (isRent) { // For rents: only size_min and size_max (no legacy 'size') const minNum = size_min !== undefined && size_min !== null ? parseFloat(size_min) : undefined; const maxNum = size_max !== undefined && size_max !== null ? parseFloat(size_max) : undefined; const hasMin = minNum !== undefined && !isNaN(minNum) && minNum >= 0; const hasMax = maxNum !== undefined && !isNaN(maxNum) && maxNum >= 0; if (hasMin && hasMax) { if (minNum <= maxNum) { baseWhere += ' AND actual_area BETWEEN ? AND ?'; params.push(minNum, maxNum); } else { // Invalid range: min > max return res.status(400).json({ success: false, message: 'size_min must be less than or equal to size_max' }); } } else if (hasMin) { baseWhere += ' AND actual_area >= ?'; params.push(minNum); } else if (hasMax) { baseWhere += ' AND actual_area <= ?'; params.push(maxNum); } } else { // For sales: support size_min/size_max AND legacy 'size' as upper bound const minNum = size_min !== undefined && size_min !== null ? parseFloat(size_min) : undefined; const maxNum = size_max !== undefined && size_max !== null ? parseFloat(size_max) : (size !== undefined && size !== null ? parseFloat(size) : undefined); const hasMin = minNum !== undefined && !isNaN(minNum) && minNum >= 0; const hasMax = maxNum !== undefined && !isNaN(maxNum) && maxNum >= 0; if (hasMin && hasMax) { if (minNum <= maxNum) { baseWhere += ' AND actual_area BETWEEN ? AND ?'; params.push(minNum, maxNum); } else { return res.status(400).json({ success: false, message: 'size_min must be less than or equal to size_max (or size)' }); } } else if (hasMin) { baseWhere += ' AND actual_area >= ?'; params.push(minNum); } else if (hasMax) { baseWhere += ' AND actual_area <= ?'; params.push(maxNum); } } // ============================================ // FILTER: Rooms/Beds - Different handling for rent vs sale // ============================================ // For sale: accept both 'rooms' and 'beds' for backward compatibility // For rent: only accept 'rooms' const roomsOrBeds = isRent ? rooms : (rooms || beds); if (roomsOrBeds !== undefined && roomsOrBeds !== null && roomsOrBeds !== '' && roomsOrBeds.toString().toLowerCase().trim() !== 'all') { if (isRent) { // For rents: use decimal value (2.0, 3.0, etc.) - exact match const roomsNum = parseFloat(roomsOrBeds); if (!isNaN(roomsNum) && roomsNum >= 0) { baseWhere += ` AND ${config.roomsColumn} = ?`; params.push(roomsNum); } // If invalid, silently ignore (matches original behavior) } else { // For sales (transactions): use string format handling like beds const bedsStr = roomsOrBeds.toString().trim(); const bedsLower = bedsStr.toLowerCase(); if (bedsLower === 'studio') { baseWhere += ` AND LOWER(${config.roomsColumn}) = ?`; params.push('studio'); } else if (bedsLower === 'null' || bedsStr === '') { baseWhere += ` AND (${config.roomsColumn} IS NULL OR ${config.roomsColumn} = "")`; } else { // Match formats like "3 b/r", "3.0", "3" baseWhere += ` AND (${config.roomsColumn} LIKE ? OR ${config.roomsColumn} = ? OR ${config.roomsColumn} = ?)`; params.push(`%${bedsStr}%`, bedsStr, `${bedsStr}.0`); } } } // ============================================ // FILTER: Project name (case-insensitive, partial match) // ============================================ if (project !== undefined && project !== null && typeof project === 'string' && project.trim() !== '' && project.toLowerCase().trim() !== 'all') { baseWhere += ' AND (LOWER(project_en) LIKE ? OR LOWER(master_project_en) LIKE ?)'; const projectName = `%${project.toLowerCase().trim()}%`; params.push(projectName, projectName); } // ============================================ // PAGINATION: Calculate pagination parameters // ============================================ // Match original endpoints exactly const rawPageSize = page_size ?? undefined; const wantsPagination = (page !== undefined) || (rawPageSize !== undefined) || (limitNum !== undefined); const defaultPage = 1; const defaultPageSize = 30; // Parse page number with validation let pageNum = defaultPage; if (page !== undefined && page !== null) { const parsed = parseInt(page, 10); if (!isNaN(parsed) && parsed >= 1) { pageNum = parsed; } } // Parse page size with validation let pageSizeNum = defaultPageSize; if (wantsPagination) { if (rawPageSize !== undefined && rawPageSize !== null) { const parsed = parseInt(rawPageSize, 10); if (!isNaN(parsed) && parsed >= 1) { pageSizeNum = parsed; } } else if (limitNum !== undefined) { pageSizeNum = limitNum; } } const offsetNum = (pageNum - 1) * pageSizeNum; // ============================================ // SQL GENERATION: Build query with pagination // ============================================ const orderBy = ` ORDER BY ${config.dateColumn} DESC, ${config.idColumn} DESC`; let dataSql; let total = null; if (!wantsPagination) { // No pagination: return all results dataSql = `SELECT * FROM ${config.tableName} ${baseWhere} ${orderBy}`; } else if (limitNum !== undefined && page === undefined && page_size === undefined) { // Legacy behavior: only limit provided (no paging), apply LIMIT without OFFSET dataSql = `SELECT * FROM ${config.tableName} ${baseWhere} ${orderBy} LIMIT ${limitNum}`; } else { // Full pagination with COUNT const countSql = `SELECT COUNT(*) AS total FROM ${config.tableName} ${baseWhere}`; try { const countRows = await database.query(countSql, params); total = countRows && countRows[0] && countRows[0].total !== undefined ? parseInt(countRows[0].total, 10) : 0; } catch (countError) { console.error('❌ Count query error:', countError); // Continue without total count if count fails total = 0; } dataSql = `SELECT * FROM ${config.tableName} ${baseWhere} ${orderBy} LIMIT ${pageSizeNum} OFFSET ${offsetNum}`; } // ============================================ // LOGGING: Debug information // ============================================ const logData = { type: normalizedType, area_name: area_name || null, property_type: property_type || null, rooms: rooms || null, beds: beds || null, project: project || null, limit: limitNum ?? null, page: pageNum, page_size: pageSizeNum }; if (isRent) { logData.size_min = size_min || null; logData.size_max = size_max || null; } else { logData.size = size || null; logData.size_min = size_min || null; logData.size_max = size_max || null; } console.log(`🔍 Fetching recent ${normalizedType} properties with filters:`, logData); // ============================================ // EXECUTE QUERY: Fetch data // ============================================ const results = await database.query(dataSql, params); // ============================================ // CALCULATE PAGINATION METADATA // ============================================ const totalPages = (total !== null && total > 0) ? Math.max(1, Math.ceil(total / pageSizeNum)) : 1; // ============================================ // BUILD FILTERS OBJECT: Match original endpoints exactly // ============================================ const filters = { type: normalizedType, area_name: area_name || null, property_type: property_type || null, project: project || null, limit: limitNum ?? null, page: total !== null ? pageNum : null, page_size: total !== null ? pageSizeNum : (limitNum !== undefined ? limitNum : null) }; // Add type-specific filters if (isRent) { filters.size_min = size_min || null; filters.size_max = size_max || null; filters.rooms = rooms || null; } else { filters.size = size || null; filters.size_min = size_min || null; filters.size_max = size_max || null; filters.rooms = rooms || null; filters.beds = beds || null; } // ============================================ // RESPONSE: Return formatted data // ============================================ res.json({ success: true, data: { [config.dataKey]: results, type: normalizedType, count: results.length, total: total !== null ? total : results.length, page: total !== null ? pageNum : null, page_size: total !== null ? pageSizeNum : (limitNum !== undefined ? limitNum : null), total_pages: total !== null ? totalPages : 1, has_next: total !== null ? pageNum < totalPages : false, has_prev: total !== null ? pageNum > 1 : false, filters: filters } }); } catch (error) { // ============================================ // ERROR HANDLING: Comprehensive error response // ============================================ const queryType = (req.query?.type || 'properties').toLowerCase(); console.error(`❌ ${queryType} query error:`, error); res.status(500).json({ success: false, message: `Failed to retrieve ${queryType}`, error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); module.exports = router;