diff --git a/src/database/migrations/20241217112401-update-reports-queries-data.js b/src/database/migrations/20241217112401-update-reports-queries-data.js index f3088ae05..16379a940 100644 --- a/src/database/migrations/20241217112401-update-reports-queries-data.js +++ b/src/database/migrations/20241217112401-update-reports-queries-data.js @@ -55,46 +55,53 @@ module.exports = { INTERVAL '1 hour' * FLOOR(SUM( CASE WHEN Session.type IN ('PUBLIC', 'PRIVATE') THEN EXTRACT(EPOCH FROM (Session.completed_at - Session.started_at)) ELSE 0 END ) / 3600) + - INTERVAL '1 minute' * FLOOR(SUM( - CASE WHEN Session.type IN ('PUBLIC', 'PRIVATE') THEN EXTRACT(EPOCH FROM (Session.completed_at - Session.started_at)) ELSE 0 END - ) / 60 % 60) + - INTERVAL '1 second' * FLOOR(SUM( - CASE WHEN Session.type IN ('PUBLIC', 'PRIVATE') THEN EXTRACT(EPOCH FROM (Session.completed_at - Session.started_at)) ELSE 0 END - ) % 60), - 'HH24:MI:SS' -) AS total_hours -, -- Total duration of all sessions - TO_CHAR( - INTERVAL '1 hour' * FLOOR(SUM(CASE WHEN Session.type = 'PUBLIC' THEN EXTRACT(EPOCH FROM (Session.completed_at - Session.started_at)) / 3600 ELSE 0 END)) + - INTERVAL '1 minute' * FLOOR(SUM(CASE WHEN Session.type = 'PUBLIC' THEN EXTRACT(EPOCH FROM (Session.completed_at - Session.started_at)) / 60 ELSE 0 END) % 60) + - INTERVAL '1 second' * FLOOR(SUM(CASE WHEN Session.type = 'PUBLIC' THEN EXTRACT(EPOCH FROM (Session.completed_at - Session.started_at)) % 60 ELSE 0 END)), - 'HH24:MI:SS' - ) AS public_hours, -- Total duration of public sessions - TO_CHAR( - INTERVAL '1 hour' * FLOOR(SUM(CASE WHEN Session.type = 'PRIVATE' THEN EXTRACT(EPOCH FROM (Session.completed_at - Session.started_at)) / 3600 ELSE 0 END)) + - INTERVAL '1 minute' * FLOOR(SUM(CASE WHEN Session.type = 'PRIVATE' THEN EXTRACT(EPOCH FROM (Session.completed_at - Session.started_at)) / 60 ELSE 0 END) % 60) + - INTERVAL '1 second' * FLOOR(SUM(CASE WHEN Session.type = 'PRIVATE' THEN EXTRACT(EPOCH FROM (Session.completed_at - Session.started_at)) % 60 ELSE 0 END)), - 'HH24:MI:SS' - ) AS private_hours - FROM - public.session_attendees AS sa - JOIN - public.sessions AS Session - ON - sa.session_id = Session.id - WHERE - (CASE WHEN :userId IS NOT NULL THEN sa.mentee_id = :userId ELSE TRUE END) - AND sa.joined_at IS NOT NULL - AND (CASE WHEN :start_date IS NOT NULL THEN Session.start_date > :start_date ELSE TRUE END) - AND (CASE WHEN :end_date IS NOT NULL THEN Session.end_date < :end_date ELSE TRUE END) + SELECT + TO_CHAR( + INTERVAL '1 hour' * FLOOR(SUM(duration) / 3600) + + INTERVAL '1 minute' * FLOOR((SUM(duration) / 60)::BIGINT % 60) + + INTERVAL '1 second' * FLOOR(SUM(duration)::BIGINT % 60), + 'HH24:MI:SS' + ) AS total_hours, -- Total duration of all sessions + + TO_CHAR( + INTERVAL '1 hour' * FLOOR(SUM(CASE WHEN type = 'PUBLIC' THEN duration ELSE 0 END) / 3600) + + INTERVAL '1 minute' * FLOOR((SUM(CASE WHEN type = 'PUBLIC' THEN duration ELSE 0 END) / 60)::BIGINT % 60) + + INTERVAL '1 second' * FLOOR(SUM(CASE WHEN type = 'PUBLIC' THEN duration ELSE 0 END)::BIGINT % 60), + 'HH24:MI:SS' + ) AS public_hours, -- Total duration of public sessions + + TO_CHAR( + INTERVAL '1 hour' * FLOOR(SUM(CASE WHEN type = 'PRIVATE' THEN duration ELSE 0 END) / 3600) + + INTERVAL '1 minute' * FLOOR((SUM(CASE WHEN type = 'PRIVATE' THEN duration ELSE 0 END) / 60)::BIGINT % 60) + + INTERVAL '1 second' * FLOOR(SUM(CASE WHEN type = 'PRIVATE' THEN duration ELSE 0 END)::BIGINT % 60), + 'HH24:MI:SS' + ) AS private_hours -- Total duration of private sessions + +FROM ( + SELECT + sa.session_id, + EXTRACT(EPOCH FROM (s.completed_at - s.started_at)) AS duration, + s.type + FROM + public.session_attendees AS sa + JOIN + public.sessions AS s + ON + sa.session_id = s.id + WHERE + (CASE WHEN :userId IS NOT NULL THEN sa.mentee_id = :userId ELSE TRUE END) + AND sa.joined_at IS NOT NULL + AND (CASE WHEN :start_date IS NOT NULL THEN s.start_date > :start_date ELSE TRUE END) + AND (CASE WHEN :end_date IS NOT NULL THEN s.end_date < :end_date ELSE TRUE END) AND ( CASE - WHEN :session_type = 'All' THEN Session.type IN ('PUBLIC', 'PRIVATE') - WHEN :session_type = 'Public' THEN Session.type = 'PUBLIC' - WHEN :session_type = 'Private' THEN Session.type = 'PRIVATE' + WHEN :session_type = 'All' THEN s.type IN ('PUBLIC', 'PRIVATE') + WHEN :session_type = 'Public' THEN s.type = 'PUBLIC' + WHEN :session_type = 'Private' THEN s.type = 'PRIVATE' ELSE TRUE END - );`, + ) +) AS session_durations`, organization_id: defaultOrgId, status: 'ACTIVE', created_at: Sequelize.literal('CURRENT_TIMESTAMP'), @@ -158,45 +165,63 @@ module.exports = { }, { report_code: 'mentee_session_details', - query: `SELECT - Session.title AS "sessions_title", - ue.name AS "sessions_created_by", - Session.mentor_name AS "mentor_name", - TO_TIMESTAMP(Session.start_date)::DATE AS "date_of_session", - Session.type AS "session_type", - ARRAY_TO_STRING(Session.categories, ', ') AS "categories", - ARRAY_TO_STRING(Session.recommended_for, ', ') AS "recommended_for", - CASE - WHEN sa.joined_at IS NOT NULL THEN 'Yes' - ELSE 'No' - END AS "session_attended", - ROUND(EXTRACT(EPOCH FROM (TO_TIMESTAMP(Session.end_date) - TO_TIMESTAMP(Session.start_date))) / 60) AS "duration_of_sessions_attended_in_minutes" - FROM ( - SELECT * - FROM public.sessions - ) AS Session -- Alias defined in the subquery - LEFT JOIN -- Moved this JOIN before the JOIN with session_attendees - public.user_extensions AS ue - ON Session.created_by = ue.user_id - JOIN -- This JOIN is now after the LEFT JOIN - public.session_attendees AS sa - ON sa.session_id = Session.id - WHERE - -- Filter by mentee ID if provided - (:userId IS NULL OR sa.mentee_id = :userId) - - -- Filter by start date if provided - AND (:start_date IS NULL OR Session.start_date > :start_date) - - -- Filter by end date if provided - AND (:end_date IS NULL OR Session.end_date < :end_date) - - -- Filter by session type - AND ( - :session_type = 'All' AND Session.type IN ('PUBLIC', 'PRIVATE') - OR :session_type = 'PUBLIC' AND Session.type = 'PUBLIC' - OR :session_type = 'PRIVATE' AND Session.type = 'PRIVATE' - );`, + query: `WITH Session AS ( + SELECT + id, + title, + created_by, + mentor_name, + start_date, + end_date, + type, + categories, + recommended_for + FROM + public.sessions + ), + UserExtensions AS ( + SELECT + user_id, + name + FROM + public.user_extensions + ), + SessionAttendees AS ( + SELECT + session_id, + mentee_id, + joined_at + FROM + public.session_attendees + ) + SELECT + Session.title AS "sessions_title", + ue.name AS "sessions_created_by", + Session.mentor_name AS "mentor_name", + TO_TIMESTAMP(Session.start_date)::DATE AS "date_of_session", + Session.type AS "session_type", + Session.categories AS "categories", + Session.recommended_for AS "recommended_for", + CASE + WHEN sa.joined_at IS NOT NULL THEN 'Yes' + ELSE 'No' + END AS "session_attended", + ROUND(EXTRACT(EPOCH FROM (TO_TIMESTAMP(Session.end_date) - TO_TIMESTAMP(Session.start_date))) / 60) AS "duration_of_sessions_attended_in_minutes" + FROM + Session + LEFT JOIN + UserExtensions AS ue ON Session.created_by = ue.user_id + JOIN + SessionAttendees AS sa ON sa.session_id = Session.id + WHERE + (:userId IS NULL OR sa.mentee_id = :userId) + AND (:start_date IS NULL OR Session.start_date > :start_date) + AND (:end_date IS NULL OR Session.end_date < :end_date) + AND ( + :session_type = 'All' AND Session.type IN ('PUBLIC', 'PRIVATE') + OR :session_type = 'PUBLIC' AND Session.type = 'PUBLIC' + OR :session_type = 'PRIVATE' AND Session.type = 'PRIVATE' + );`, organization_id: defaultOrgId, status: 'ACTIVE', created_at: Sequelize.literal('CURRENT_TIMESTAMP'), @@ -239,28 +264,38 @@ module.exports = { { report_code: 'total_hours_of_mentoring_conducted', query: `WITH filtered_ownerships AS ( - SELECT - so.session_id - FROM - public.session_ownerships so + SELECT so.session_id + FROM public.session_ownerships so WHERE - so.user_id = :userId - AND so.type = 'MENTOR' - -- Filter based on the user_id + so.user_id = :userId + AND so.type = 'MENTOR' ) + SELECT -- Total duration (sum of both public and private sessions) COALESCE( TO_CHAR( INTERVAL '1 hour' * FLOOR(SUM( - CASE WHEN session.type IN ('PUBLIC', 'PRIVATE') THEN EXTRACT(EPOCH FROM (session.completed_at - session.started_at)) ELSE 0 END + CASE + WHEN s.type IN ('PUBLIC', 'PRIVATE') + THEN EXTRACT(EPOCH FROM (s.completed_at - s.started_at)) + ELSE 0 + END ) / 3600) + - INTERVAL '1 minute' * FLOOR(SUM( - CASE WHEN session.type IN ('PUBLIC', 'PRIVATE') THEN EXTRACT(EPOCH FROM (session.completed_at - session.started_at)) ELSE 0 END - ) / 60 % 60) + + INTERVAL '1 minute' * FLOOR((SUM( + CASE + WHEN s.type IN ('PUBLIC', 'PRIVATE') + THEN EXTRACT(EPOCH FROM (s.completed_at - s.started_at)) + ELSE 0 + END + ) / 60)::BIGINT % 60) + INTERVAL '1 second' * FLOOR(SUM( - CASE WHEN session.type IN ('PUBLIC', 'PRIVATE') THEN EXTRACT(EPOCH FROM (session.completed_at - session.started_at)) ELSE 0 END - ) % 60), + CASE + WHEN s.type IN ('PUBLIC', 'PRIVATE') + THEN EXTRACT(EPOCH FROM (s.completed_at - s.started_at)) + ELSE 0 + END + )::BIGINT % 60), 'HH24:MI:SS' ), '00:00:00' @@ -270,14 +305,26 @@ module.exports = { COALESCE( TO_CHAR( INTERVAL '1 hour' * FLOOR(SUM( - CASE WHEN session.type = 'PUBLIC' THEN EXTRACT(EPOCH FROM (session.completed_at - session.started_at)) ELSE 0 END + CASE + WHEN s.type = 'PUBLIC' + THEN EXTRACT(EPOCH FROM (s.completed_at - s.started_at)) + ELSE 0 + END ) / 3600) + - INTERVAL '1 minute' * FLOOR(SUM( - CASE WHEN session.type = 'PUBLIC' THEN EXTRACT(EPOCH FROM (session.completed_at - session.started_at)) ELSE 0 END - ) / 60 % 60) + + INTERVAL '1 minute' * FLOOR((SUM( + CASE + WHEN s.type = 'PUBLIC' + THEN EXTRACT(EPOCH FROM (s.completed_at - s.started_at)) + ELSE 0 + END + ) / 60)::BIGINT % 60) + INTERVAL '1 second' * FLOOR(SUM( - CASE WHEN session.type = 'PUBLIC' THEN EXTRACT(EPOCH FROM (session.completed_at - session.started_at)) ELSE 0 END - ) % 60), + CASE + WHEN s.type = 'PUBLIC' + THEN EXTRACT(EPOCH FROM (s.completed_at - s.started_at)) + ELSE 0 + END + )::BIGINT % 60), 'HH24:MI:SS' ), '00:00:00' @@ -287,34 +334,44 @@ module.exports = { COALESCE( TO_CHAR( INTERVAL '1 hour' * FLOOR(SUM( - CASE WHEN session.type = 'PRIVATE' THEN EXTRACT(EPOCH FROM (session.completed_at - session.started_at)) ELSE 0 END + CASE + WHEN s.type = 'PRIVATE' + THEN EXTRACT(EPOCH FROM (s.completed_at - s.started_at)) + ELSE 0 + END ) / 3600) + - INTERVAL '1 minute' * FLOOR(SUM( - CASE WHEN session.type = 'PRIVATE' THEN EXTRACT(EPOCH FROM (session.completed_at - session.started_at)) ELSE 0 END - ) / 60 % 60) + + INTERVAL '1 minute' * FLOOR((SUM( + CASE + WHEN s.type = 'PRIVATE' + THEN EXTRACT(EPOCH FROM (s.completed_at - s.started_at)) + ELSE 0 + END + ) / 60)::BIGINT % 60) + INTERVAL '1 second' * FLOOR(SUM( - CASE WHEN session.type = 'PRIVATE' THEN EXTRACT(EPOCH FROM (session.completed_at - session.started_at)) ELSE 0 END - ) % 60), + CASE + WHEN s.type = 'PRIVATE' + THEN EXTRACT(EPOCH FROM (s.completed_at - s.started_at)) + ELSE 0 + END + )::BIGINT % 60), 'HH24:MI:SS' ), '00:00:00' ) AS private_hours - FROM - filtered_ownerships fo - JOIN - public.sessions session ON session.id = fo.session_id -- Join with the sessions table based on session_id - WHERE - session.status = 'COMPLETED' - AND session.start_date > :start_date -- Start date filter - AND session.end_date < :end_date -- End date filter - AND ( - CASE - WHEN :session_type = 'All' THEN session.type IN ('PUBLIC', 'PRIVATE') -- If all types, include both - WHEN :session_type = 'PUBLIC' THEN session.type = 'PUBLIC' -- If PUBLIC, only include public - WHEN :session_type = 'PRIVATE' THEN session.type = 'PRIVATE' -- If PRIVATE, only include private - ELSE TRUE -- Default condition - END - );`, + + FROM filtered_ownerships fo + JOIN public.sessions s ON s.id = fo.session_id -- Renamed alias from session to s + WHERE s.status = 'COMPLETED' + AND s.start_date > :start_date -- Start date filter + AND s.end_date < :end_date -- End date filter + AND ( + CASE + WHEN :session_type = 'All' THEN s.type IN ('PUBLIC', 'PRIVATE') -- If all types, include both + WHEN :session_type = 'PUBLIC' THEN s.type = 'PUBLIC' -- If PUBLIC, only include public + WHEN :session_type = 'PRIVATE' THEN s.type = 'PRIVATE' -- If PRIVATE, only include private + ELSE TRUE -- Default condition + END + );`, organization_id: defaultOrgId, status: 'ACTIVE', created_at: Sequelize.literal('CURRENT_TIMESTAMP'), @@ -430,13 +487,12 @@ module.exports = { query: `SELECT session.title AS "sessions_title", ue.name AS "sessions_created_by", - session.seats_limit - session.seats_remaining AS "number_of_mentees", + session.seats_limit-session.seats_remaining AS "number_of_mentees", TO_TIMESTAMP(session.start_date)::DATE AS "date_of_session", session.type AS "session_type", CASE WHEN session.started_at IS NOT NULL THEN 'Yes' ELSE 'No' END AS "session_conducted", - ROUND(EXTRACT(EPOCH FROM (TO_TIMESTAMP(session.end_date) - TO_TIMESTAMP(session.start_date))) / 60) AS "duration_of_sessions_attended_in_minutes" - FROM - (SELECT * FROM public.sessions WHERE start_date > :start_date AND end_date < :end_date) AS session + ROUND(EXTRACT(EPOCH FROM(TO_TIMESTAMP(session.end_date)-TO_TIMESTAMP(session.start_date)))/60) AS "duration_of_sessions_attended_in_minutes" + FROM (SELECT * FROM public.sessions WHERE start_date > :start_date AND end_date < :end_date) AS session JOIN (SELECT * FROM public.session_ownerships WHERE user_id = :userId AND type = 'MENTOR') AS so ON session.id = so.session_id LEFT JOIN @@ -622,42 +678,28 @@ module.exports = { }, { report_code: 'session_manger_session_details', - query: `SELECT - subquery."mentor_name" , - subquery."number_of_mentoring_sessions", - subquery."hours_of_mentoring_sessions", - subquery."avg_mentor_rating" - FROM ( - SELECT - session.mentor_name AS "mentor_name", - COUNT(*) OVER (PARTITION BY so.user_id) AS "number_of_mentoring_sessions", - CASE - WHEN - ROUND(SUM(EXTRACT(EPOCH FROM (session.completed_at - session.started_at))) / 3600.0) = FLOOR(SUM(EXTRACT(EPOCH FROM (session.completed_at - session.started_at))) / 3600.0) - THEN - CAST(FLOOR(SUM(EXTRACT(EPOCH FROM (session.completed_at - session.started_at))) / 3600.0) AS TEXT) - ELSE - CAST(ROUND(SUM(EXTRACT(EPOCH FROM (session.completed_at - session.started_at))) / 3600.0, 1) AS TEXT) - END AS "hours_of_mentoring_sessions", - COALESCE(CAST(ue.rating ->> 'average' AS NUMERIC), 0) AS "avg_mentor_rating" - FROM - (SELECT * FROM public.sessions WHERE created_by = :userId AND started_at IS NOT NULL AND completed_at IS NOT NULL AND start_date > :start_date AND end_date < :end_date AND ( - CASE - WHEN :session_type = 'All' THEN type IN ('PUBLIC', 'PRIVATE') - WHEN :session_type = 'PUBLIC' THEN type = 'PUBLIC' - WHEN :session_type = 'PRIVATE' THEN type = 'PRIVATE' - ELSE TRUE - END - )) AS session - JOIN (SELECT DISTINCT on (session_id, user_id) * FROM public.session_ownerships WHERE type IN ('CREATOR', 'MENTOR')) AS so ON session.id = so.session_id - LEFT JOIN - public.user_extensions AS ue ON so.user_id = ue.user_id - GROUP BY - so.user_id, - session.created_by, - session.mentor_name, - COALESCE(CAST(ue.rating ->> 'average' AS NUMERIC), 0) - ) AS subquery + query: `WITH + session_count AS ( + SELECT s.mentor_id, s.mentor_name, COUNT(*) AS number_of_sessions, + TO_CHAR( INTERVAL '1 second' * ROUND(SUM(EXTRACT(EPOCH FROM (s.completed_at - s.started_at)))), + 'HH24:MI:SS' + ) AS "hours_of_mentoring_sessions" + FROM public.sessions AS s WHERE s.created_by = :userId AND s.started_at IS NOT NULL AND s.completed_at IS NOT NULL AND s.start_date > :start_date AND s.end_date < :end_date AND (CASE + WHEN :session_type = 'All' THEN s.type IN ('PUBLIC', 'PRIVATE') + WHEN :session_type = 'Public' THEN s.type = 'PUBLIC' + WHEN :session_type = 'Private' THEN s.type = 'PRIVATE' + ELSE TRUE + END + ) + GROUP BY s.mentor_id, s.mentor_name + ) + SELECT sc.mentor_name as mentor_name , + sc.number_of_sessions as number_of_mentoring_sessions, + sc.hours_of_mentoring_sessions as hours_of_mentoring_sessions, + COALESCE(CAST(ue.rating ->>'average'AS NUMERIC),0) AS avg_mentor_rating + FROM session_count AS sc + JOIN public.user_extensions AS ue ON sc.mentor_id = ue.user_id + ORDER BY sc.mentor_name ;`, organization_id: defaultOrgId, status: 'ACTIVE', diff --git a/src/generics/utils.js b/src/generics/utils.js index 715022642..8d833f757 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -899,20 +899,30 @@ const mapEntityTypesToData = (data, entityTypes) => { } function extractColumnMappings(sqlQuery) { - // Match the SELECT part of the query - const selectMatch = sqlQuery.match(/SELECT\s+(.*?)\s+FROM /is) - if (!selectMatch) return {} // Return an empty object if no match is found + // Match the entire WITH clause including its closing bracket `)` and locate SELECT after it + const withMatch = sqlQuery.match(/WITH\s+[^]*?\)\s*GROUP BY\s+[^]*?\s*SELECT\s+(.*?)\s+FROM\s+\w+/is) - const selectPart = selectMatch[1] + if (withMatch) { + // Extract only the `SELECT` part after `WITH (...)` + return processSelectPart(withMatch[1].trim()) + } else { + // If no WITH clause, look for a regular SELECT query + const selectMatch = sqlQuery.match(/SELECT\s+(.*?)\s+FROM /is) + if (!selectMatch) return {} // Return empty object if no match is found + + return processSelectPart(selectMatch[1].trim()) + } +} - // Split columns by commas, but ignore commas inside parentheses (to avoid splitting function calls) +function processSelectPart(selectPart) { + // Split columns by commas, ignoring commas inside functions (parentheses) const columns = selectPart.split(/,(?![^\(\)]*\))/).map((col) => col.trim()) const columnMappings = {} columns.forEach((column) => { - // Match alias expressions like 'TO_TIMESTAMP(s.start_date)::DATE AS "date_of_session"' - const aliasMatch = column.match(/(.*?)\s+AS\s+"(.*?)"/i) + // Match alias expressions like `COUNT(*) AS number_of_sessions` + const aliasMatch = column.match(/(.*?)\s+AS\s+"?(.*?)"?$/i) if (aliasMatch) { const original = aliasMatch[1].trim() @@ -929,6 +939,7 @@ function extractColumnMappings(sqlQuery) { } else { columnMappings[alias] = original } + // columnMappings[alias] = original; } else { // Handle case where there is no alias (if any) let cleanColumn = column.trim() @@ -969,87 +980,94 @@ function applyDefaultFilters(filters, columnConfigs) { function getDynamicFilterCondition(filters, columnMappings, baseQuery, columnConfig) { if (!filters || typeof filters !== 'object') { console.log('Filters is not an object or is empty') - return '' // Early exit if filters are not valid + return baseQuery // Return the base query unchanged } const conditions = Object.entries(filters) .map(([column, value]) => { - const mappedColumn = columnMappings[column] + let mappedColumn = columnMappings[column] if (!mappedColumn) { console.log(`No mapping found for column: ${column}`) return null // Skip if no mapping is found for the column } - // Find the filterType for the column from columnConfig const columnConfigEntry = columnConfig.find((config) => config.key === column) const filterType = columnConfigEntry ? columnConfigEntry.filterType || '=' : '=' // Default to '=' if not found - // Special case: Handle the column with ROUND(EXTRACT...) logic - if (mappedColumn.includes('ROUND(EXTRACT')) { - if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) { - console.error( - `Invalid filter value for column ${column}. Expected an array of strings but received ${typeof value}.` - ) - return null + + // Handle time filtering dynamically + if (column === 'hours_of_mentoring_sessions') { + if (typeof value === 'string' && value.includes(':')) { + // Convert HH:MM:SS to total seconds + const [hh, mm, ss] = value.split(':').map(Number) + const totalSeconds = hh * 3600 + mm * 60 + (ss || 0) + return `total_mentoring_seconds ${filterType} ${totalSeconds}` + } else if (typeof value === 'number') { + // If value is already in seconds, use it directly + return `total_mentoring_seconds ${filterType} ${value}` } - return `${mappedColumn} ${filterType} '${value}'` + console.error(`Invalid time format for filtering ${column}`) + return null } if (value) { if (Array.isArray(value)) { - // If value is an array, combine with OR for multiple values - const arrayConditions = value + if ( + Array.isArray(value) && + (mappedColumn.includes('categories') || mappedColumn.includes('recommended_for')) + ) { + const conditions = value.map((val) => `'${val}' = ANY(${mappedColumn})`).join(' OR ') + return `(${conditions})` // Wrap in parentheses for clarity/precedence + } + + return `(${value .map((val) => { if (val instanceof Date) { - // Handle Date objects return `${mappedColumn} ${filterType} TO_TIMESTAMP('${val.toISOString()}', 'YYYY-MM-DD"T"HH24:MI:SS')` } else if (typeof val === 'string' && isStrictValidDate(val)) { - // Handle string-based date values return `${mappedColumn} ${filterType} TO_TIMESTAMP('${val}', 'YYYY-MM-DD')` } else if (typeof val === 'number') { - // Handle numeric values return `${mappedColumn} ${filterType} ${val}` } - // Handle general string values return `${mappedColumn} ${filterType} '${val}'` }) - .join(' OR ') // Join array conditions with OR - return `(${arrayConditions})` + .join(' OR ')})` } else if (value instanceof Date) { - // Handle single Date object return `${mappedColumn} ${filterType} TO_TIMESTAMP('${value.toISOString()}', 'YYYY-MM-DD"T"HH24:MI:SS')` } else if (typeof value === 'string' && isStrictValidDate(value)) { - // Handle single string-based date values return `${mappedColumn} ${filterType} TO_TIMESTAMP('${value}', 'YYYY-MM-DD')` } else if (typeof value === 'number') { - // Handle single numeric values return `${mappedColumn} ${filterType} ${value}` } - // Handle other value types as strings return `${mappedColumn} ${filterType} '${value}'` } - return null // Return null if no valid value exists for the filter + return null }) - .filter(Boolean) // Remove null entries (where no condition was generated) + .filter(Boolean) - const conditionsString = conditions.join('\nAND ') // Join all conditions with AND - - // Check if baseQuery already has WHERE conditions - const hasWhereClause = baseQuery.includes('WHERE') - const hasGroupBy = baseQuery.includes('GROUP BY') - - // Append conditions to the query + const conditionsString = conditions.join(' AND ') if (conditionsString) { - if (hasGroupBy) { - // Append before GROUP BY clause if it exists - return `${hasWhereClause ? 'WHERE' : 'AND'} ${conditionsString}` + const hasWhereClause = /\bWHERE\b/i.test(baseQuery) // More robust WHERE check + const hasOrderBy = /\bORDER BY\b/i.test(baseQuery) // More robust ORDER BY check + + let modifiedBaseQuery = baseQuery + + if (hasOrderBy) { + const orderByMatch = baseQuery.match(/\bORDER BY\b.*$/is) // Match ORDER BY clause more precisely + if (orderByMatch) { + const whereClause = hasWhereClause ? ` AND ${conditionsString}` : ` WHERE ${conditionsString}` + modifiedBaseQuery = modifiedBaseQuery.replace( + orderByMatch[0], + `${whereClause} \n${orderByMatch[0].trimStart()}` + ) // Trim leading whitespace + } } else { - // Standard WHERE clause logic - return `${hasWhereClause ? 'AND' : 'WHERE'} ${conditionsString}` + const whereClause = hasWhereClause ? ` AND ${conditionsString}` : ` WHERE ${conditionsString}` + modifiedBaseQuery = baseQuery + whereClause } - } - // Return an empty string if no conditions were generated + return modifiedBaseQuery + } return '' } @@ -1145,22 +1163,29 @@ function getDynamicSearchCondition(search, columnMappings, baseQuery) { }) .filter(Boolean) // Remove null entries - const conditionsString = conditions.join('\nAND ') // Join all conditions with AND - - // Check if baseQuery already has WHERE conditions - const hasWhereClause = baseQuery.includes('WHERE') - const hasGroupBy = baseQuery.includes('GROUP BY') - - // Append conditions to the query + const conditionsString = conditions.join(' AND ') if (conditionsString) { - if (hasGroupBy === false) { - return `${hasWhereClause ? 'AND' : 'WHERE'} ${conditionsString}` + const hasWhereClause = /\bWHERE\b/i.test(baseQuery) // More robust WHERE check + const hasOrderBy = /\bORDER BY\b/i.test(baseQuery) // More robust ORDER BY check + + let modifiedBaseQuery = baseQuery + + if (hasOrderBy) { + const orderByMatch = baseQuery.match(/\bORDER BY\b.*$/is) // Match ORDER BY clause more precisely + if (orderByMatch) { + const whereClause = hasWhereClause ? ` AND ${conditionsString}` : ` WHERE ${conditionsString}` + modifiedBaseQuery = modifiedBaseQuery.replace( + orderByMatch[0], + `${whereClause} \n${orderByMatch[0].trimStart()}` + ) // Trim leading whitespace + } } else { - return `${hasWhereClause ? 'WHERE' : 'AND'} ${conditionsString}` + const whereClause = hasWhereClause ? ` AND ${conditionsString}` : ` WHERE ${conditionsString}` + modifiedBaseQuery = baseQuery + whereClause } - } - // Return an empty string if no conditions were generated + return modifiedBaseQuery + } return '' } @@ -1203,7 +1228,10 @@ const mapEntityTypeToData = (data, entityTypes) => { // If the key exists in the data item if (newItem[key]) { - const values = newItem[key].split(',').map((val) => val.trim()) + const values = newItem[key] + .toString() + .split(',') + .map((val) => val.trim()) // Map values to corresponding entity labels const mappedValues = values diff --git a/src/services/reports.js b/src/services/reports.js index 0a906fe35..bf039b100 100644 --- a/src/services/reports.js +++ b/src/services/reports.js @@ -337,7 +337,11 @@ module.exports = class ReportsHelper { columnConfig.columns ) if (filterConditions) { - query += filterConditions + if (query.includes('WITH' && 'ORDER BY')) { + query = filterConditions + } else { + query = filterConditions + } } } @@ -350,7 +354,11 @@ module.exports = class ReportsHelper { columnConfig.columns ) if (searchConditions) { - query += searchConditions + if (query.includes('WITH' && 'ORDER BY')) { + query = searchConditions + } else { + query = searchConditions + } } }