diff --git a/DSL/Resql/analytics/POST/feedback-buerokratt-chats-nps.sql b/DSL/Resql/analytics/POST/feedback-buerokratt-chats-nps.sql index 68eb18e4..7ed890f2 100644 --- a/DSL/Resql/analytics/POST/feedback-buerokratt-chats-nps.sql +++ b/DSL/Resql/analytics/POST/feedback-buerokratt-chats-nps.sql @@ -13,8 +13,8 @@ chat_buerokratt AS ( ORDER BY updated ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS ended, - CASE - WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) + CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN last_value(feedback_rating_five) OVER ( PARTITION BY base_id ORDER BY updated @@ -30,7 +30,7 @@ chat_buerokratt AS ( WHERE ( array_length(ARRAY[:urls]::TEXT[], 1) IS NULL OR chat.end_user_url LIKE ANY(ARRAY[:urls]::TEXT[]) - ) + ) AND ( :showTest = TRUE OR chat.test = FALSE @@ -42,8 +42,8 @@ chat_buerokratt AS ( AND message.author_role = 'buerokratt' ) AND status = 'ENDED' - AND CASE - WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) + AND CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN feedback_rating_five IS NOT NULL ELSE feedback_rating IS NOT NULL END @@ -51,28 +51,50 @@ chat_buerokratt AS ( ), point_nps AS ( SELECT date_trunc(:metric, ended)::text AS date_time, - COALESCE( - CAST(( - ( - SUM(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 ELSE 0 END) * 1.0 - - SUM(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 ELSE 0 END) - ) / NULLIF(COUNT(feedback_rating_dynamic), 0) * 100 - ) AS int), 0) AS nps + CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN + ROUND( + 100.0 * SUM(CASE WHEN feedback_rating_dynamic IN (4, 5) THEN 1 ELSE 0 END) + / NULLIF(COUNT(feedback_rating_dynamic), 0), + 2 + ) + ELSE + COALESCE(ROUND( + ( + (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(feedback_rating_dynamic), 0)) + - (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(feedback_rating_dynamic), 0)) + ) * 100, + 2 + ), 0) + END AS nps FROM chat_buerokratt GROUP BY date_time ORDER BY date_time ), period_nps AS ( - SELECT COALESCE( - CAST(( - ( - SUM(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 ELSE 0 END) * 1.0 - - SUM(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 ELSE 0 END) - ) / NULLIF(COUNT(feedback_rating_dynamic), 0) * 100 - ) AS int), 0) AS nps + SELECT CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN + ROUND( + 100.0 * SUM(CASE WHEN feedback_rating_dynamic IN (4, 5) THEN 1 ELSE 0 END) + / NULLIF(COUNT(feedback_rating_dynamic), 0), + 2 + ) + ELSE + COALESCE(ROUND( + ( + (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(feedback_rating_dynamic), 0)) + - (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(feedback_rating_dynamic), 0)) + ) * 100, + 2 + ), 0) + END AS nps FROM chat_buerokratt +), +is_five AS ( + SELECT COALESCE(is_five_rating_scale, 'false') = 'true' AS is_five_scale FROM rating_config ) SELECT json_build_object( 'pointNps', (SELECT json_agg(json_build_object('dateTime', date_time, 'nps', nps)) FROM point_nps), - 'periodNps', (SELECT nps FROM period_nps) -) AS result + 'periodNps', (SELECT nps FROM period_nps), + 'isFiveScale', (SELECT is_five_scale FROM is_five) +) AS result \ No newline at end of file diff --git a/DSL/Resql/analytics/POST/feedback-chats-distribution.sql b/DSL/Resql/analytics/POST/feedback-chats-distribution.sql index 142f7e3f..72e8f47b 100644 --- a/DSL/Resql/analytics/POST/feedback-chats-distribution.sql +++ b/DSL/Resql/analytics/POST/feedback-chats-distribution.sql @@ -7,14 +7,14 @@ WITH rating_config AS ( AND NOT deleted ), chats_filtered AS ( - SELECT DISTINCT + SELECT DISTINCT base_id, first_value(created) OVER ( PARTITION BY base_id ORDER BY updated ) AS created, - CASE - WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) + CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN last_value(feedback_rating_five) OVER ( PARTITION BY base_id ORDER BY updated @@ -28,44 +28,97 @@ chats_filtered AS ( WHERE ( array_length(ARRAY[:urls]::TEXT[], 1) IS NULL OR chat.end_user_url LIKE ANY(ARRAY[:urls]::TEXT[]) - ) + ) AND ( :showTest = TRUE OR chat.test = FALSE - ) + ) AND STATUS = 'ENDED' - AND CASE - WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) + AND CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN feedback_rating_five IS NOT NULL ELSE feedback_rating IS NOT NULL END AND created::timestamptz BETWEEN :start::timestamptz AND :end::timestamptz AND ( (:chat_type = 'buerokratt' AND EXISTS ( - SELECT 1 - FROM message - WHERE message.chat_base_id = chat.base_id + SELECT 1 + FROM message + WHERE message.chat_base_id = chat.base_id AND message.author_role = 'buerokratt' )) - OR + OR (:chat_type = 'csa' AND customer_support_id <> '' AND EXISTS ( - SELECT 1 - FROM message - WHERE message.chat_base_id = chat.base_id + SELECT 1 + FROM message + WHERE message.chat_base_id = chat.base_id AND message.author_role = 'backoffice-user' ) AND EXISTS ( - SELECT 1 - FROM message - WHERE message.chat_base_id = chat.base_id + SELECT 1 + FROM message + WHERE message.chat_base_id = chat.base_id AND message.author_role = 'end-user' ) ) ) +), +all_ended_chats AS ( + SELECT COUNT(DISTINCT base_id) AS total_chats + FROM chat + WHERE ( + array_length(ARRAY[:urls]::TEXT[], 1) IS NULL + OR chat.end_user_url LIKE ANY(ARRAY[:urls]::TEXT[]) + ) + AND (:showTest = TRUE OR chat.test = FALSE) + AND STATUS = 'ENDED' + AND created::timestamptz BETWEEN :start::timestamptz AND :end::timestamptz + AND ( + (:chat_type = 'buerokratt' AND EXISTS ( + SELECT 1 FROM message WHERE message.chat_base_id = chat.base_id AND message.author_role = 'buerokratt' + )) + OR + (:chat_type = 'csa' AND customer_support_id <> '' + AND EXISTS ( + SELECT 1 FROM message WHERE message.chat_base_id = chat.base_id AND message.author_role = 'backoffice-user' + ) + AND EXISTS ( + SELECT 1 FROM message WHERE message.chat_base_id = chat.base_id AND message.author_role = 'end-user' + ) + ) + ) +), +rating_counts AS ( + SELECT feedback_rating_dynamic AS rating, COUNT(*) AS cnt + FROM chats_filtered + GROUP BY feedback_rating_dynamic +), +scale_ratings AS ( + SELECT generate_series AS rating + FROM ( + SELECT generate_series( + CASE WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN 1 ELSE 0 END, + CASE WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN 5 ELSE 10 END + ) + ) s +), +no_feedback_count AS ( + SELECT (SELECT total_chats FROM all_ended_chats) - (SELECT COUNT(*) FROM chats_filtered) AS cnt +), +distribution_with_no_feedback AS ( + SELECT json_agg(elem ORDER BY ord, rating_nullable NULLS LAST) AS distribution + FROM ( + SELECT 0 AS ord, sr.rating AS rating_nullable, json_build_object('rating', sr.rating, 'count', COALESCE(rc.cnt, 0)) AS elem + FROM scale_ratings sr + LEFT JOIN rating_counts rc ON sr.rating = rc.rating + UNION ALL + SELECT 1 AS ord, NULL::int AS rating_nullable, json_build_object('rating', '-', 'count', (SELECT cnt FROM no_feedback_count)) AS elem + ) parts ) -SELECT - COUNT(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 END) AS promoters, - COUNT(CASE WHEN feedback_rating_dynamic BETWEEN 7 AND 8 THEN 1 END) AS passives, - COUNT(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 END) AS detractors -FROM chats_filtered; +SELECT json_build_object( + 'distribution', (SELECT distribution FROM distribution_with_no_feedback), + 'total_feedback', (SELECT COUNT(*) FROM chats_filtered), + 'total_chats', (SELECT total_chats FROM all_ended_chats), + 'is_five_scale', (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) +) AS result; diff --git a/DSL/Resql/analytics/POST/feedback-csa-chats-feedback-nps.sql b/DSL/Resql/analytics/POST/feedback-csa-chats-feedback-nps.sql index 1ec3bff5..af111367 100644 --- a/DSL/Resql/analytics/POST/feedback-csa-chats-feedback-nps.sql +++ b/DSL/Resql/analytics/POST/feedback-csa-chats-feedback-nps.sql @@ -8,18 +8,18 @@ WITH rating_config AS ( ), chat_csas AS ( SELECT DISTINCT base_id, - first_value(created) over ( - PARTITION by base_id + first_value(created) OVER ( + PARTITION BY base_id ORDER BY updated ) AS created, - CASE - WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) - THEN last_value(feedback_rating_five) over ( - PARTITION by base_id + CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) + THEN last_value(feedback_rating_five) OVER ( + PARTITION BY base_id ORDER BY updated ) - ELSE last_value(feedback_rating) over ( - PARTITION by base_id + ELSE last_value(feedback_rating) OVER ( + PARTITION BY base_id ORDER BY updated ) END AS feedback_rating_dynamic @@ -40,8 +40,8 @@ chat_csas AS ( AND message.author_role = 'end-user' ) AND STATUS = 'ENDED' - AND CASE - WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) + AND CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN feedback_rating_five IS NOT NULL ELSE feedback_rating IS NOT NULL END @@ -49,22 +49,50 @@ chat_csas AS ( ), point_nps AS ( SELECT date_trunc(:metric, created)::text AS date_time, - coalesce(CAST((( - SUM(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 ELSE 0 END) * 1.0 - - SUM(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 ELSE 0 END) - ) / COUNT(base_id) * 100) AS int), 0) AS nps + CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN + ROUND( + 100.0 * SUM(CASE WHEN feedback_rating_dynamic IN (4, 5) THEN 1 ELSE 0 END) + / NULLIF(COUNT(base_id), 0), + 2 + ) + ELSE + COALESCE(ROUND( + ( + (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(base_id), 0)) + - (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(base_id), 0)) + ) * 100, + 2 + ), 0) + END AS nps FROM chat_csas GROUP BY date_time ORDER BY date_time ), period_nps AS ( - SELECT coalesce(CAST((( - SUM(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 ELSE 0 END) * 1.0 - - SUM(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 ELSE 0 END) - ) / COUNT(base_id) * 100) AS int), 0) AS nps + SELECT CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN + ROUND( + 100.0 * SUM(CASE WHEN feedback_rating_dynamic IN (4, 5) THEN 1 ELSE 0 END) + / NULLIF(COUNT(base_id), 0), + 2 + ) + ELSE + COALESCE(ROUND( + ( + (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(base_id), 0)) + - (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(base_id), 0)) + ) * 100, + 2 + ), 0) + END AS nps FROM chat_csas +), +is_five AS ( + SELECT COALESCE(is_five_rating_scale, 'false') = 'true' AS is_five_scale FROM rating_config ) SELECT json_build_object( 'pointNps', (SELECT json_agg(json_build_object('dateTime', date_time, 'nps', nps)) FROM point_nps), - 'periodNps', (SELECT nps FROM period_nps) -) AS result + 'periodNps', (SELECT nps FROM period_nps), + 'isFiveScale', (SELECT is_five_scale FROM is_five) +) AS result \ No newline at end of file diff --git a/DSL/Resql/analytics/POST/feedback-selected-csa-chats-distribution.sql b/DSL/Resql/analytics/POST/feedback-selected-csa-chats-distribution.sql new file mode 100644 index 00000000..be60ddf2 --- /dev/null +++ b/DSL/Resql/analytics/POST/feedback-selected-csa-chats-distribution.sql @@ -0,0 +1,111 @@ +WITH rating_config AS ( + SELECT value AS is_five_rating_scale + FROM configuration + WHERE key = 'isFiveRatingScale' + AND "domain" IS NULL + AND id IN (SELECT max(id) FROM configuration WHERE key = 'isFiveRatingScale' AND "domain" IS NULL) + AND NOT deleted +), +chats_filtered AS ( + SELECT DISTINCT + base_id, + first_value(created) OVER ( + PARTITION BY base_id + ORDER BY updated + ) AS created, + CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) + THEN last_value(feedback_rating_five) OVER ( + PARTITION BY base_id + ORDER BY updated + ) + ELSE last_value(feedback_rating) OVER ( + PARTITION BY base_id + ORDER BY updated + ) + END AS feedback_rating_dynamic + FROM chat + WHERE ( + array_length(ARRAY[:urls]::TEXT[], 1) IS NULL + OR chat.end_user_url LIKE ANY(ARRAY[:urls]::TEXT[]) + ) + AND ( + :showTest = TRUE + OR chat.test = FALSE + ) + AND STATUS = 'ENDED' + AND customer_support_id NOT IN (:excluded_csas) + AND customer_support_id <> '' + AND customer_support_id <> 'chatbot' + AND CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) + THEN feedback_rating_five IS NOT NULL + ELSE feedback_rating IS NOT NULL + END + AND created::timestamptz BETWEEN :start::timestamptz AND :end::timestamptz + AND EXISTS ( + SELECT 1 + FROM message + WHERE message.chat_base_id = chat.base_id + AND message.author_role = 'backoffice-user' + ) + AND EXISTS ( + SELECT 1 + FROM message + WHERE message.chat_base_id = chat.base_id + AND message.author_role = 'end-user' + ) +), +all_ended_chats AS ( + SELECT COUNT(DISTINCT base_id) AS total_chats + FROM chat + WHERE ( + array_length(ARRAY[:urls]::TEXT[], 1) IS NULL + OR chat.end_user_url LIKE ANY(ARRAY[:urls]::TEXT[]) + ) + AND (:showTest = TRUE OR chat.test = FALSE) + AND STATUS = 'ENDED' + AND created::timestamptz BETWEEN :start::timestamptz AND :end::timestamptz + AND customer_support_id NOT IN (:excluded_csas) + AND customer_support_id <> '' + AND customer_support_id <> 'chatbot' + AND EXISTS ( + SELECT 1 FROM message WHERE message.chat_base_id = chat.base_id AND message.author_role = 'backoffice-user' + ) + AND EXISTS ( + SELECT 1 FROM message WHERE message.chat_base_id = chat.base_id AND message.author_role = 'end-user' + ) +), +rating_counts AS ( + SELECT feedback_rating_dynamic AS rating, COUNT(*) AS cnt + FROM chats_filtered + GROUP BY feedback_rating_dynamic +), +scale_ratings AS ( + SELECT generate_series AS rating + FROM ( + SELECT generate_series( + CASE WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN 1 ELSE 0 END, + CASE WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN 5 ELSE 10 END + ) + ) s +), +no_feedback_count AS ( + SELECT (SELECT total_chats FROM all_ended_chats) - (SELECT COUNT(*) FROM chats_filtered) AS cnt +), +distribution_with_no_feedback AS ( + SELECT json_agg(elem ORDER BY ord, rating_nullable NULLS LAST) AS distribution + FROM ( + SELECT 0 AS ord, sr.rating AS rating_nullable, json_build_object('rating', sr.rating, 'count', COALESCE(rc.cnt, 0)) AS elem + FROM scale_ratings sr + LEFT JOIN rating_counts rc ON sr.rating = rc.rating + UNION ALL + SELECT 1 AS ord, NULL::int AS rating_nullable, json_build_object('rating', '-', 'count', (SELECT cnt FROM no_feedback_count)) AS elem + ) parts +) +SELECT json_build_object( + 'distribution', (SELECT distribution FROM distribution_with_no_feedback), + 'total_feedback', (SELECT COUNT(*) FROM chats_filtered), + 'total_chats', (SELECT total_chats FROM all_ended_chats), + 'is_five_scale', (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) +) AS result; diff --git a/DSL/Resql/analytics/POST/feedback-selected-csa-feedback-nps.sql b/DSL/Resql/analytics/POST/feedback-selected-csa-feedback-nps.sql index 2fa9ae55..98de3c19 100644 --- a/DSL/Resql/analytics/POST/feedback-selected-csa-feedback-nps.sql +++ b/DSL/Resql/analytics/POST/feedback-selected-csa-feedback-nps.sql @@ -15,8 +15,8 @@ ranked_chats AS ( OR chat.end_user_url LIKE ANY(ARRAY[:urls]::TEXT[]) ) AND customer_support_id NOT IN ('', 'chatbot') AND STATUS = 'ENDED' - AND CASE - WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) + AND CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN feedback_rating_five IS NOT NULL ELSE feedback_rating IS NOT NULL END @@ -34,8 +34,8 @@ chat_csas AS ( created, customer_support_id, customer_support_display_name, - CASE - WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) + CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN feedback_rating_five ELSE feedback_rating END AS feedback_rating_dynamic @@ -46,10 +46,18 @@ point_nps_by_csa AS ( SELECT date_trunc(:metric, created)::text AS date_time, customer_support_id, TRIM(customer_support_display_name) AS customer_support_display_name, - COALESCE(CAST((( - SUM(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 ELSE 0 END) * 1.0 - - SUM(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 ELSE 0 END) - ) / COUNT(base_id) * 100) AS int), 0) AS nps + CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN + ROUND(100.0 * SUM(CASE WHEN feedback_rating_dynamic IN (4, 5) THEN 1 ELSE 0 END) / NULLIF(COUNT(base_id), 0), 2) + ELSE + COALESCE(ROUND( + ( + (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(base_id), 0)) + - (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(base_id), 0)) + ) * 100, + 2 + ), 0) + END AS nps FROM chat_csas GROUP BY date_time, customer_support_id, customer_support_display_name ), @@ -57,10 +65,18 @@ period_nps_by_csa AS ( SELECT customer_support_id, TRIM(customer_support_display_name) AS customer_support_display_name, MAX(CONCAT("user".first_name, ' ', "user".last_name)) AS customer_support_full_name, - COALESCE(CAST((( - SUM(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 ELSE 0 END) * 1.0 - - SUM(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 ELSE 0 END) - ) / COUNT(base_id) * 100) AS int), 0) AS period_nps + CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN + ROUND(100.0 * SUM(CASE WHEN feedback_rating_dynamic IN (4, 5) THEN 1 ELSE 0 END) / NULLIF(COUNT(base_id), 0), 2) + ELSE + COALESCE(ROUND( + ( + (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(base_id), 0)) + - (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(base_id), 0)) + ) * 100, + 2 + ), 0) + END AS period_nps FROM chat_csas LEFT JOIN "user" ON "user".id_code = chat_csas.customer_support_id GROUP BY customer_support_id, customer_support_display_name @@ -73,4 +89,4 @@ SELECT p.date_time, t.period_nps FROM point_nps_by_csa p JOIN period_nps_by_csa t ON p.customer_support_id = t.customer_support_id -ORDER BY p.date_time, p.customer_support_display_name +ORDER BY p.date_time, p.customer_support_display_name \ No newline at end of file diff --git a/DSL/Resql/analytics/POST/feedback-selected-csa-nps-aggregate.sql b/DSL/Resql/analytics/POST/feedback-selected-csa-nps-aggregate.sql new file mode 100644 index 00000000..832a54c9 --- /dev/null +++ b/DSL/Resql/analytics/POST/feedback-selected-csa-nps-aggregate.sql @@ -0,0 +1,100 @@ +WITH rating_config AS ( + SELECT value AS is_five_rating_scale + FROM configuration + WHERE key = 'isFiveRatingScale' + AND "domain" IS NULL + AND id IN (SELECT max(id) FROM configuration WHERE key = 'isFiveRatingScale' AND "domain" IS NULL) + AND NOT deleted +), +chat_csas AS ( + SELECT DISTINCT base_id, + first_value(created) OVER ( + PARTITION BY base_id + ORDER BY updated + ) AS created, + CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) + THEN last_value(feedback_rating_five) OVER ( + PARTITION BY base_id + ORDER BY updated + ) + ELSE last_value(feedback_rating) OVER ( + PARTITION BY base_id + ORDER BY updated + ) + END AS feedback_rating_dynamic + FROM chat + WHERE ( + array_length(ARRAY[:urls]::TEXT[], 1) IS NULL + OR chat.end_user_url LIKE ANY(ARRAY[:urls]::TEXT[]) + ) + AND ( + :showTest = TRUE + OR chat.test = FALSE + ) + AND customer_support_id NOT IN (:excluded_csas) + AND customer_support_id <> '' + AND customer_support_id <> 'chatbot' + AND EXISTS ( + SELECT 1 + FROM message + WHERE message.chat_base_id = chat.base_id + AND message.author_role = 'end-user' + ) + AND STATUS = 'ENDED' + AND CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) + THEN feedback_rating_five IS NOT NULL + ELSE feedback_rating IS NOT NULL + END + AND created::timestamptz BETWEEN :start::timestamptz AND :end::timestamptz +), +point_nps AS ( + SELECT date_trunc(:metric, created)::text AS date_time, + CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN + ROUND( + 100.0 * SUM(CASE WHEN feedback_rating_dynamic IN (4, 5) THEN 1 ELSE 0 END) + / NULLIF(COUNT(base_id), 0), + 2 + ) + ELSE + COALESCE(ROUND( + ( + (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(base_id), 0)) + - (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(base_id), 0)) + ) * 100, + 2 + ), 0) + END AS nps + FROM chat_csas + GROUP BY date_time + ORDER BY date_time +), +period_nps AS ( + SELECT CASE + WHEN (SELECT COALESCE(is_five_rating_scale, 'false') = 'true' FROM rating_config) THEN + ROUND( + 100.0 * SUM(CASE WHEN feedback_rating_dynamic IN (4, 5) THEN 1 ELSE 0 END) + / NULLIF(COUNT(base_id), 0), + 2 + ) + ELSE + COALESCE(ROUND( + ( + (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 9 AND 10 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(base_id), 0)) + - (SUM(CASE WHEN feedback_rating_dynamic BETWEEN 0 AND 6 THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(base_id), 0)) + ) * 100, + 2 + ), 0) + END AS nps + FROM chat_csas +), +is_five AS ( + SELECT COALESCE(is_five_rating_scale, 'false') = 'true' AS is_five_scale FROM rating_config +) +SELECT json_build_object( + 'pointNps', (SELECT json_agg(json_build_object('dateTime', date_time, 'nps', nps)) FROM point_nps), + 'periodNps', (SELECT nps FROM period_nps), + 'isFiveScale', (SELECT is_five_scale FROM is_five) +) AS result; diff --git a/DSL/Ruuter/analytics/POST/feedbacks/agents/distribution.yml b/DSL/Ruuter/analytics/POST/feedbacks/agents/distribution.yml new file mode 100644 index 00000000..59ede49e --- /dev/null +++ b/DSL/Ruuter/analytics/POST/feedbacks/agents/distribution.yml @@ -0,0 +1,52 @@ +declaration: + call: declare + version: 0.1 + description: "Distribution of feedback for selected CSAs" + method: post + accepts: json + returns: json + namespace: analytics + allowlist: + body: + - field: end_date + type: string + description: "Body field 'end_date'" + - field: start_date + type: string + description: "Body field 'start_date'" + - field: urls + type: array + description: "Body field 'urls'" + - field: showTest + type: boolean + description: "Body field 'showTest'" + - field: excluded_csas + type: array + description: "Body field 'excluded_csas'" + +check_for_required_parameters: + switch: + - condition: ${incoming.body == null || incoming.body.start_date == null || incoming.body.end_date == null || incoming.body.excluded_csas == null} + next: return_incorrect_request + next: post_step + +post_step: + call: http.post + args: + url: "[#ANALYTICS_RESQL]/feedback-selected-csa-chats-distribution" + body: + start: ${incoming.body.start_date} + end: ${incoming.body.end_date} + urls: ${incoming.body.urls} + showTest: ${incoming.body.showTest} + excluded_csas: ${incoming.body.excluded_csas} + result: result + +return_value: + return: ${result.response.body} + next: end + +return_incorrect_request: + status: 400 + return: 'missing parameters' + next: end diff --git a/DSL/Ruuter/analytics/POST/feedbacks/agents/nps-aggregate.yml b/DSL/Ruuter/analytics/POST/feedbacks/agents/nps-aggregate.yml new file mode 100644 index 00000000..9363b2f0 --- /dev/null +++ b/DSL/Ruuter/analytics/POST/feedbacks/agents/nps-aggregate.yml @@ -0,0 +1,68 @@ +declaration: + call: declare + version: 0.1 + description: "Aggregate NPS for selected CSAs (same chart shape as advisor conversations)" + method: post + accepts: json + returns: json + namespace: analytics + allowlist: + body: + - field: end_date + type: string + description: "Body field 'end_date'" + - field: excluded_csas + type: array + description: "Body field 'excluded_csas'" + - field: metric + type: string + description: "Body field 'metric'" + - field: start_date + type: string + description: "Body field 'start_date'" + - field: urls + type: array + description: "Body field 'urls'" + - field: showTest + type: boolean + description: "Body field 'showTest'" + +check_for_required_parameters: + switch: + - condition: ${incoming.body == null || incoming.body.metric == null || incoming.body.start_date == null || incoming.body.end_date == null || incoming.body.excluded_csas == null} + next: return_incorrect_request + next: post_step + +post_step: + call: http.post + args: + url: "[#ANALYTICS_RESQL]/feedback-selected-csa-nps-aggregate" + body: + metric: ${incoming.body.metric} + start: ${incoming.body.start_date} + end: ${incoming.body.end_date} + excluded_csas: ${incoming.body.excluded_csas} + urls: ${incoming.body.urls} + showTest: ${incoming.body.showTest} + result: result + +get_nps_object: + call: http.post + args: + url: "[#ANALYTICS_DMAPPER_HBS]/get-csa-nps-object" + headers: + type: "json" + body: + pointNps: ${JSON.parse(result.response.body[0].result.value).pointNps ?? []} + periodNps: ${JSON.parse(result.response.body[0].result.value).periodNps} + result: nps + +return_value: + wrapper: false + return: ${nps.response.body} + next: end + +return_incorrect_request: + status: 400 + return: "missing parameters" + next: end diff --git a/GUI/src/components/BarGraph/index.tsx b/GUI/src/components/BarGraph/index.tsx index 3a94842b..33610055 100644 --- a/GUI/src/components/BarGraph/index.tsx +++ b/GUI/src/components/BarGraph/index.tsx @@ -15,15 +15,18 @@ import { ChartData } from 'types/chart'; import { usePeriodStatisticsContext } from 'hooks/usePeriodStatisticsContext'; import { CustomChartTooltip } from 'components'; +const FEEDBACK_Y_AXIS_MAX = 20; + type Props = { data: ChartData; startDate: string; endDate: string; unit?: string; groupByPeriod: GroupByPeriod; + isRatingDistribution?: boolean; }; -const BarGraph: React.FC = ({ startDate, endDate, data, unit, groupByPeriod }) => { +const BarGraph: React.FC = ({ startDate, endDate, data, unit, groupByPeriod, isRatingDistribution }) => { const [width, setWidth] = useState(null); const { periodStatistics } = usePeriodStatisticsContext(); @@ -48,6 +51,46 @@ const BarGraph: React.FC = ({ startDate, endDate, data, unit, groupByPeri const domain = [minDate, new Date(endDate).getTime()]; const ticks = getTicks(startDate, endDate, new Date(startDate), new Date(endDate), 5); + const ratingTooltip = (props: { payload?: Array<{ payload?: { rating: number | string; count: number } }> }) => { + const payload = props?.payload ?? []; + if (!payload.length) return null; + const p = payload[0]?.payload; + if (p?.count == null) return null; + const ratingLabel = p.rating === '-' ? t('feedback.chatsWithNoFeedback') : String(p.rating); + return ( +
+ {String(t('chart.rating'))}: {ratingLabel} — {String(t('chart.count'))}: {p.count} +
+ ); + }; + + if (isRatingDistribution && (data?.chartData?.length ?? 0) > 0 && data.chartData?.[0] && ('rating' in data.chartData[0] || 'displayCount' in data.chartData[0])) { + return ( +
+ + + + + + + + +
+ ); + } + return (
{ +const LineGraph = ({ data, startDate, endDate, unit, isRatingDistribution }: Props) => { const [width, setWidth] = useState(null); const ref = useRef(null); const { periodStatistics } = usePeriodStatisticsContext(); + const { t } = useTranslation(); useEffect(() => { const handleResize = () => { @@ -37,6 +42,45 @@ const LineGraph = ({ data, startDate, endDate, unit }: Props) => { const domain = [new Date(startDate).getTime(), new Date(endDate).getTime()]; const ticks = getTicks(startDate, endDate, new Date(startDate), new Date(endDate), 5); + const ratingTooltip = (props: { payload?: Array<{ payload?: { rating: number | string; count: number } }> }) => { + const payload = props?.payload ?? []; + if (!payload.length) return null; + const p = payload[0]?.payload; + if (p?.count == null) return null; + const ratingLabel = p.rating === '-' ? t('feedback.chatsWithNoFeedback') : String(p.rating); + return ( +
+ {String(t('chart.rating'))}: {ratingLabel} — {String(t('chart.count'))}: {p.count} +
+ ); + }; + + if (isRatingDistribution && (data?.chartData?.length ?? 0) > 0 && data.chartData?.[0] && ('rating' in data.chartData[0] || 'displayCount' in data.chartData[0])) { + return ( +
+ + + + + + + + +
+ ); + } + return (
{ + if (value == null || Number.isNaN(value)) return '—'; + const n = Number(value); + return Number.isFinite(n) ? n.toFixed(2) : '—'; +}; + const MetricsCharts = ({ title, data, startDate, endDate, unit, groupByPeriod }: Props) => { const { t } = useTranslation(); const formattedStartDate = formatDate(new Date(startDate), 'yyyy-MM-dd'); const formattedEndDate = formatDate(new Date(endDate), 'yyyy-MM-dd'); + const feedbackScoreLabel = data.distributionData?.isFiveScale ? t('feedback.positiveFeedbackScore') : t('feedback.averageNps'); const charts: ChartType[] = [ { @@ -42,11 +49,13 @@ const MetricsCharts = ({ title, data, startDate, endDate, unit, groupByPeriod }: }, ]; const [selectedChart, setSelectedChart] = useState('barChart'); - const selectedData = selectedChart === 'pieChart' ? (data.distributionData ?? data) : (data.feedBackData ?? data); + const isRatingDistribution = data.distributionData?.isRatingDistribution === true; + const distributionOrFeedBack = selectedChart === 'pieChart' ? (data.distributionData ?? data) : (data.feedBackData ?? data); + const selectedData = isRatingDistribution ? (data.distributionData ?? data) : distributionOrFeedBack; const buildChart = () => { if (selectedChart === 'pieChart') { - return ; + return ; } else if (selectedChart === 'lineChart') { return ( ); } else { @@ -64,6 +74,7 @@ const MetricsCharts = ({ title, data, startDate, endDate, unit, groupByPeriod }: endDate={formattedEndDate} unit={unit} groupByPeriod={groupByPeriod} + isRatingDistribution={isRatingDistribution} /> ); } @@ -126,7 +137,13 @@ const MetricsCharts = ({ title, data, startDate, endDate, unit, groupByPeriod }: appearance="text" style={{ marginRight: 15 }} onClick={() => { - downloadXlsx(data.feedBackData ? data.feedBackData?.chartData : data.chartData); + let sourceData = data.chartData; + if (data.distributionData?.isRatingDistribution) { + sourceData = data.distributionData?.chartData ?? data.chartData; + } else if (data.feedBackData?.chartData) { + sourceData = data.feedBackData.chartData; + } + downloadXlsx(sourceData); }} > {buildChart()}
+ {isRatingDistribution && (data.distributionData?.totalChats != null || data.distributionData?.totalFeedback != null) && ( +
+
+ + {feedbackScoreLabel}: {formatPeriodScore(data.feedBackData?.periodNps ?? data.periodNps)} + +
+
+ {t('feedback.percentOfChatsWithFeedback')}:{' '} + {data.distributionData?.totalChats != null && data.distributionData.totalChats > 0 + ? `${((data.distributionData.totalFeedback ?? 0) / data.distributionData.totalChats * 100).toFixed(1)}%` + : '0%'} +
+
+ {t('feedback.chatsWithNoFeedback')}: {data.distributionData?.noFeedbackCount ?? (data.distributionData?.totalChats != null && data.distributionData?.totalFeedback != null ? data.distributionData.totalChats - data.distributionData.totalFeedback : '—')} +
+
+ )} ); }; diff --git a/GUI/src/components/PieGraph/index.tsx b/GUI/src/components/PieGraph/index.tsx index ecd8e686..043ddaad 100644 --- a/GUI/src/components/PieGraph/index.tsx +++ b/GUI/src/components/PieGraph/index.tsx @@ -12,14 +12,32 @@ import { ChartData } from 'types/chart'; type Props = { data: ChartData; + isRatingDistribution?: boolean; }; -const PieGraph = ({ data }: Props) => { +const PieGraph = ({ data, isRatingDistribution }: Props) => { const [width, setWidth] = useState(null); const ref = useRef(null); const { t } = useTranslation(); - const percentages = useMemo(() => calculatePercentagesFromResponse(data?.chartData ?? []), [data?.chartData]); + const percentages = useMemo(() => { + const chartData = data?.chartData ?? []; + if (isRatingDistribution && chartData.length > 0 && ('rating' in chartData[0] || 'count' in chartData[0])) { + const typed = chartData as { rating: number | string; count: number }[]; + const total = typed.reduce((s, d) => s + d.count, 0); + return typed.map((d) => ({ + name: String(d.rating), + value: total === 0 ? 0 : Math.round((d.count / total) * 1000) / 10, + })); + } + return calculatePercentagesFromResponse( + chartData.map(obj => + Object.fromEntries( + Object.entries(obj).map(([k, v]) => [k, typeof v === 'number' ? v : Number(v)]) + ) + ) as Record[] + ); + }, [data?.chartData, isRatingDistribution]); useEffect(() => { const handleResize = () => { diff --git a/GUI/src/components/services/user.ts b/GUI/src/components/services/user.ts index a6961b7e..82af8822 100644 --- a/GUI/src/components/services/user.ts +++ b/GUI/src/components/services/user.ts @@ -1,8 +1,8 @@ import { DomainSelection } from '../../types/widgetModels'; -import { api } from './api'; +import { analyticsApi } from './api'; export async function getWidgetData(userId: string) { - const { data } = await api.get('accounts/widget-data', { + const { data } = await analyticsApi.get('accounts/widget-data', { params: { user_id: userId, }, diff --git a/GUI/src/i18n/en/common.json b/GUI/src/i18n/en/common.json index 705d24b7..9478bcc0 100644 --- a/GUI/src/i18n/en/common.json +++ b/GUI/src/i18n/en/common.json @@ -158,7 +158,10 @@ "ended": "Ended", "view": "View", "comment": "Comment", - "feedback": "Feedback" + "positiveFeedbackScore": "Positive feedback (%)", + "averageNps": "Average (NPS)", + "percentOfChatsWithFeedback": "% of chats that received feedback", + "chatsWithNoFeedback": "No Feedback" }, "chats": { "total": "Total number of chats", @@ -194,7 +197,6 @@ "event": "Event", "avg": "Average", "nps": "NPS", - "customerSupportFullName": "CSA Nimi", "id": "Id", "baseId": "Id", "rating": "Rating", diff --git a/GUI/src/i18n/et/common.json b/GUI/src/i18n/et/common.json index eadabda9..3b42dc09 100644 --- a/GUI/src/i18n/et/common.json +++ b/GUI/src/i18n/et/common.json @@ -158,7 +158,10 @@ "ended": "Lõppenud", "view": "Vaata", "comment": "Kommentaar", - "feedback": "Tagasiside" + "positiveFeedbackScore": "Positiivne tagasiside (%)", + "averageNps": "Keskmine (NPS)", + "percentOfChatsWithFeedback": "% vestlustest, millele anti tagasiside", + "chatsWithNoFeedback": "Tagasisidet pole" }, "chats": { "total": "Vestluste koguarv", @@ -195,7 +198,6 @@ "avg": "Keskmine", "nps": "NPS", "avgWaitingTimeSeconds": "Keskmine (Sec)", - "customerSupportFullName": "CSA Full Name", "id": "Id", "baseId": "Id", "rating": "Hinnang", diff --git a/GUI/src/pages/FeedbackPage.tsx b/GUI/src/pages/FeedbackPage.tsx index a3c950bf..9e879a7b 100644 --- a/GUI/src/pages/FeedbackPage.tsx +++ b/GUI/src/pages/FeedbackPage.tsx @@ -3,10 +3,11 @@ import React, {useEffect, useRef, useState} from 'react'; import OptionsPanel, {Option} from '../components/MetricAndPeriodOptions'; import MetricsCharts from '../components/MetricsCharts'; import { - getAverageFeedbackOnBuerokrattChats, getChatsStatuses, getDistributionOnBuerokrattChatsFeedback, getDistributionOnCSAChatsFeedback, + getDistributionOnSelectedCSAChatsFeedback, + getNpsAggregateOnSelectedCSAChatsFeedback, getNpsFeedbackOnBuerokrattChats, getNpsOnCSAChatsFeedback, getNpsOnSelectedCSAChatsFeedback, @@ -33,6 +34,8 @@ import {getDomainsArray} from "../util/multiDomain-utils"; import {getShowTestData} from "../util/testChat-utils"; import { endOfDay, formatISO, startOfDay } from 'date-fns'; +const FEEDBACK_Y_AXIS_MAX = 20; + const statusOptions = [ 'CLIENT_LEFT_WITH_ACCEPTED', 'CLIENT_LEFT_WITH_NO_RESOLUTION', @@ -105,29 +108,17 @@ const FeedbackPage: React.FC = () => { { id: 'burokratt_chats', labelKey: 'feedback.burokratt_chats', - unit: t('units.nps') ?? 'nps', - subRadioOptions: [ - { - id: 'NPS', - labelKey: 'feedback.status_options.nps', - color: randomColor(), - }, - { - id: 'AVG', - labelKey: 'feedback.status_options.average', - color: randomColor(), - }, - ], + unit: '', }, { id: 'advisor_chats', labelKey: 'feedback.advisor_chats', - unit: t('units.nps') ?? 'nps', + unit: '', }, { id: 'selected_advisor_chats', labelKey: 'feedback.selected_advisor_chats', - unit: t('units.nps') ?? 'nps', + unit: '', }, { id: 'negative_feedback', @@ -152,13 +143,10 @@ const FeedbackPage: React.FC = () => { case 'statuses': return fetchChatsStatuses(config); case 'burokratt_chats': { - const promises = [ + const [distributionData, feedBackData] = await Promise.all([ fetchDistributionOnBuerokrattChatsFeedback(config), - config.options === 'AVG' - ? fetchAverageFeedbackOnBuerokrattChats(config) - : fetchNpsFeedbackOnBuerokrattChats(config), - ]; - const [distributionData, feedBackData] = await Promise.all(promises); + fetchNpsFeedbackOnBuerokrattChats(config), + ]); return {distributionData, feedBackData}; } case 'advisor_chats': { @@ -168,8 +156,14 @@ const FeedbackPage: React.FC = () => { ]); return {distributionData, feedBackData}; } - case 'selected_advisor_chats': - return fetchNpsOnSelectedCSAChatsFeedback(config); + case 'selected_advisor_chats': { + const [distributionData, feedBackData] = await Promise.all([ + fetchDistributionOnSelectedCSAChatsFeedback(config), + fetchNpsAggregateOnSelectedCSAChatsFeedback(config), + ]); + await fetchNpsOnSelectedCSAChatsFeedback(config); + return {distributionData, feedBackData}; + } case 'negative_feedback': return {}; default: @@ -246,24 +240,6 @@ const FeedbackPage: React.FC = () => { return chartData; }; - const fetchAverageFeedbackOnBuerokrattChats = async (config: any) => { - setShowSelectAll(false); - let chartData = {}; - try { - const {response} = await fetchAndMapFeedbackData(getAverageFeedbackOnBuerokrattChats, config); - - chartData = { - chartData: response, - colors: [{ id: 'average', color: '#FFB511' }], - minPointSize: 3, - }; - setUnit(t('units.minutes') ?? 'chats'); - } catch (err) { - console.error("Failed: ", err) - } - return chartData; - }; - const fetchNpsFeedbackOnBuerokrattChats = async (config: any) => { setShowSelectAll(false); let chartData = {}; @@ -297,7 +273,8 @@ const FeedbackPage: React.FC = () => { }, }); - chartData = mapDistributionChartData(result); + const body = result.response ?? result; + chartData = mapDistributionChartData(body); } catch (e) { console.error(e); } @@ -336,19 +313,79 @@ const FeedbackPage: React.FC = () => { showTest: getShowTestData() }, }); + const body = result.response ?? result; + chartData = mapDistributionChartData(body); + } catch (e) { + console.error(e); + } + return chartData; + }; + + const getExcludedCsas = (config: any) => { + const excluded_csas = advisors.current.map((e: any) => e.id).filter((e: string) => !config?.options?.includes(e)); + return (excluded_csas.length ?? 0) > 0 ? excluded_csas : ['']; + }; - chartData = mapDistributionChartData(result); + const fetchDistributionOnSelectedCSAChatsFeedback = async (config: any) => { + setShowSelectAll(true); + let chartData = {}; + try { + const result: any = await request({ + url: getDistributionOnSelectedCSAChatsFeedback(), + method: Methods.post, + withCredentials: true, + data: { + start_date: config?.start, + end_date: config?.end, + urls: getDomainsArray(), + showTest: getShowTestData(), + excluded_csas: getExcludedCsas(config), + }, + }); + const body = result.response ?? result; + chartData = mapDistributionChartData(body); } catch (e) { console.error(e); } return chartData; }; + const fetchNpsAggregateOnSelectedCSAChatsFeedback = async (config: any) => { + setShowSelectAll(true); + let chartData = {}; + try { + const result: any = await request({ + url: getNpsAggregateOnSelectedCSAChatsFeedback(), + method: Methods.post, + withCredentials: true, + data: { + metric: config?.groupByPeriod ?? 'day', + start_date: config?.start, + end_date: config?.end, + urls: getDomainsArray(), + showTest: getShowTestData(), + excluded_csas: getExcludedCsas(config), + }, + }); + const response = result.response.map((entry: any) => ({ + ...translateChartKeys(entry, chartDataKey), + [chartDataKey]: new Date(entry[chartDataKey]).getTime(), + })); + chartData = { + chartData: response, + colors: [{ id: 'NPS', color: '#FFB511' }], + periodNps: result.periodNps, + }; + } catch (err) { + console.error('Failed: ', err); + } + return chartData; + }; + const fetchNpsOnSelectedCSAChatsFeedback = async (config: any) => { setShowSelectAll(true); let chartData = {}; try { - const excluded_csas = advisors.current.map((e) => e.id).filter((e) => !config?.options.includes(e)); const result: any = await request({ url: getNpsOnSelectedCSAChatsFeedback(), method: Methods.post, @@ -357,7 +394,7 @@ const FeedbackPage: React.FC = () => { metric: config?.groupByPeriod ?? 'day', start_date: config?.start, end_date: config?.end, - excluded_csas: (excluded_csas.length ?? 0) > 0 ? excluded_csas : [''], + excluded_csas: getExcludedCsas(config), urls: getDomainsArray(), showTest: getShowTestData() }, @@ -422,22 +459,35 @@ const FeedbackPage: React.FC = () => { return {result, response}; }; - const mapDistributionChartData = (result: any) => { - const {promoters, passives, detractors} = result.response[0]; + const mapDistributionChartData = (result: any, isFiveScale?: boolean) => { + const response = result.response ?? result; + const raw = Array.isArray(response) ? response[0] : response; + const data = raw?.result?.value ? JSON.parse(raw.result.value) : (raw?.result ?? raw); + const distribution: { rating: number | string; count: number }[] = data?.distribution ?? []; + const totalFeedback = data?.total_feedback ?? 0; + const totalChats = data?.total_chats ?? 0; + const scaleIsFive = data?.is_five_scale ?? isFiveScale ?? false; + const noFeedbackCount = totalChats - totalFeedback; + + const chartData = distribution.map((r: { rating: number | string; count: number }) => ({ + rating: r.rating, + count: r.count, + displayCount: Math.min(r.count, FEEDBACK_Y_AXIS_MAX), + })); + + const colors = chartData.map((d: { rating: number | string }) => ({ + id: String(d.rating), + color: randomColor(), + })); + return { - chartData: - promoters === 0 && passives === 0 && detractors === 0 - ? [] - : [ - {[t('chart.promoters')]: promoters}, - {[t('chart.passives')]: passives}, - {[t('chart.detractors')]: detractors}, - ], - colors: [ - {id: t('chart.promoters'), color: '#FF0000'}, - {id: t('chart.passives'), color: '#0000FF'}, - {id: t('chart.detractors'), color: '#00FF00'}, - ], + chartData, + colors, + isRatingDistribution: true, + totalFeedback, + totalChats, + noFeedbackCount, + isFiveScale: scaleIsFive, }; }; @@ -459,7 +509,7 @@ const FeedbackPage: React.FC = () => { } } /> - {currentConfigs?.metric != 'negative_feedback' && ( + {currentConfigs?.metric !== 'negative_feedback' && ( { unit={unit} /> )} - {showNegativeChart && + {showNegativeChart && ( { user={useStore.getState().userInfo} userDomains={useStore} /> - } + )} ); }; diff --git a/GUI/src/resources/api-constants.ts b/GUI/src/resources/api-constants.ts index 698e3357..8a71915f 100644 --- a/GUI/src/resources/api-constants.ts +++ b/GUI/src/resources/api-constants.ts @@ -1,144 +1,152 @@ -const baseUrl = import.meta.env.REACT_APP_RUUTER_V2_ANALYTICS_API_URL; -const ruuterUrl = import.meta.env.REACT_APP_DOCKER_RUUTER; - -export const openSearchDashboard = 'https://opensearch.org/'; - -export const getLinkToChat = (chatId: string, startDate?: string, endDate?: string) => - `/chat/history?chat=${chatId}&start=${startDate}&end=${endDate}`; - -export const getOpenDataValues = (lang: string): string => baseUrl + '/odp/values?lang=' + lang; - -export const openDataSettings = (): string => baseUrl + '/odp/settings'; -export const deleteOpenDataSettings = (): string => baseUrl + '/odp/delete-settings'; - -export const openDataDataset = (): string => baseUrl + '/odp/dataset'; -export const getOpenDataDataset = (datasetId: string): string => baseUrl + '/odp/dataset?datasetId=' + datasetId; - -export const scheduledReports = (): string => baseUrl + '/odp/scheduled-reports'; -export const editScheduledReport = (): string => baseUrl + '/odp/update-scheduled-report'; -export const deleteScheduledReport = (): string => baseUrl + '/odp/delete-scheduled-report'; -export const uploadScheduledReport = (datasetId: string, dateTime: string): string => - ruuterUrl + '/odp/upload-scheduled-report?datasetId=' + datasetId + '&dateTime=' + dateTime; -export const deleteCronJobTask = (): string => baseUrl + '/odp/delete-cron-job-task'; - -export const downloadOpenDataXlsx = (): string => baseUrl + '/odp/download'; - -export const saveJsonToYaml = (): string => baseUrl + '/saveJsonToYml'; - -export const getTesting = (): string => { - return baseUrl + '/testing'; -}; - -export const overviewMetricPreferences = (): string => { - return baseUrl + '/overview/preferences'; -}; - -export const overviewMetrics = (): string => { - return baseUrl + `/overview/metrics`; -}; - -export const geBykAvgResponseTime = (): string => { - return baseUrl + '/bots/avg-response-speed'; -}; - -export const getBykAvgSessionTime = (): string => { - return baseUrl + '/bots/avg-sessions-time'; -}; - -export const getBykIntents = (): string => { - return baseUrl + '/bots/intents'; -}; - -export const getBykPercentOfCorrecltyUnderstood = (): string => { - return baseUrl + '/bots/pct-correctly-understood'; -}; - -export const getXlsx = (): string => { - return baseUrl + '/xlsx'; -}; - -// Feedback - -export const getChatsStatuses = (): string => { - return baseUrl + '/chats/status'; -}; - -export const getAverageFeedbackOnBuerokrattChats = (): string => { - return baseUrl + '/feedbacks/avg'; -}; - -export const getNpsFeedbackOnBuerokrattChats = (): string => { - return baseUrl + '/feedbacks/buerokratt-chats-nps'; -}; - -export const getNpsOnCSAChatsFeedback = (): string => { - return baseUrl + '/feedbacks/nps'; -}; - -export const getDistributionOnBuerokrattChatsFeedback = (): string => { - return baseUrl + '/feedbacks/buerokratt-chats-distribution'; -}; - -export const getDistributionOnCSAChatsFeedback = (): string => { - return baseUrl + '/feedbacks/csa-chats-distribution'; -}; - -export const getNpsOnSelectedCSAChatsFeedback = (): string => { - return baseUrl + '/feedbacks/agents/nps'; -}; - -export const getNegativeFeedbackChats = (): string => { - return baseUrl + '/feedbacks/negative'; -}; - -// Advisors - -export const getChatForwards = (): string => { - return baseUrl + '/agents/chats/forwards'; -}; - -export const getAvgPickTime = (): string => { - return baseUrl + '/agents/chats/avg-time-picking-up'; -}; - -export const getAvgCsaPresent = (): string => { - return baseUrl + '/agents/avg-active'; -}; - -export const getCsaChatsTotal = (): string => { - return baseUrl + '/agents/chats/total'; -}; - -export const getCsaAvgChatTime = (): string => { - return baseUrl + '/agents/chats/avg-time'; -}; - -// Chats - -export const getTotalChats = (): string => { - return baseUrl + '/chats/total-count'; -}; - -export const getCipChats = (): string => { - return baseUrl + '/chats/contact-information-fulfilled'; -}; - -export const getAvgChatWaitingTime = (): string => { - return baseUrl + '/chats/avg-median-waiting-time'; -}; - -export const getAvgMessagesInChats = (): string => { - return baseUrl + '/chats/avg-num-of-messages'; -}; - -export const getDurationChats = (): string => { - return baseUrl + '/chats/avg-duration'; -}; - -export const getIdleChats = (): string => { - return baseUrl + '/chats/idle-count'; -}; - -export const getBykEndedChats = (): string => { - return baseUrl + '/chats/byk-ended-count'; -}; +const baseUrl = import.meta.env.REACT_APP_RUUTER_V2_ANALYTICS_API_URL; +const ruuterUrl = import.meta.env.REACT_APP_DOCKER_RUUTER; + +export const openSearchDashboard = 'https://opensearch.org/'; + +export const getLinkToChat = (chatId: string, startDate?: string, endDate?: string) => + `/chat/history?chat=${chatId}&start=${startDate}&end=${endDate}`; + +export const getOpenDataValues = (lang: string): string => baseUrl + '/odp/values?lang=' + lang; + +export const openDataSettings = (): string => baseUrl + '/odp/settings'; +export const deleteOpenDataSettings = (): string => baseUrl + '/odp/delete-settings'; + +export const openDataDataset = (): string => baseUrl + '/odp/dataset'; +export const getOpenDataDataset = (datasetId: string): string => baseUrl + '/odp/dataset?datasetId=' + datasetId; + +export const scheduledReports = (): string => baseUrl + '/odp/scheduled-reports'; +export const editScheduledReport = (): string => baseUrl + '/odp/update-scheduled-report'; +export const deleteScheduledReport = (): string => baseUrl + '/odp/delete-scheduled-report'; +export const uploadScheduledReport = (datasetId: string, dateTime: string): string => + ruuterUrl + '/odp/upload-scheduled-report?datasetId=' + datasetId + '&dateTime=' + dateTime; +export const deleteCronJobTask = (): string => baseUrl + '/odp/delete-cron-job-task'; + +export const downloadOpenDataXlsx = (): string => baseUrl + '/odp/download'; + +export const saveJsonToYaml = (): string => baseUrl + '/saveJsonToYml'; + +export const getTesting = (): string => { + return baseUrl + '/testing'; +}; + +export const overviewMetricPreferences = (): string => { + return baseUrl + '/overview/preferences'; +}; + +export const overviewMetrics = (): string => { + return baseUrl + `/overview/metrics`; +}; + +export const geBykAvgResponseTime = (): string => { + return baseUrl + '/bots/avg-response-speed'; +}; + +export const getBykAvgSessionTime = (): string => { + return baseUrl + '/bots/avg-sessions-time'; +}; + +export const getBykIntents = (): string => { + return baseUrl + '/bots/intents'; +}; + +export const getBykPercentOfCorrecltyUnderstood = (): string => { + return baseUrl + '/bots/pct-correctly-understood'; +}; + +export const getXlsx = (): string => { + return baseUrl + '/xlsx'; +}; + +// Feedback + +export const getChatsStatuses = (): string => { + return baseUrl + '/chats/status'; +}; + +export const getAverageFeedbackOnBuerokrattChats = (): string => { + return baseUrl + '/feedbacks/avg'; +}; + +export const getNpsFeedbackOnBuerokrattChats = (): string => { + return baseUrl + '/feedbacks/buerokratt-chats-nps'; +}; + +export const getNpsOnCSAChatsFeedback = (): string => { + return baseUrl + '/feedbacks/nps'; +}; + +export const getDistributionOnBuerokrattChatsFeedback = (): string => { + return baseUrl + '/feedbacks/buerokratt-chats-distribution'; +}; + +export const getDistributionOnCSAChatsFeedback = (): string => { + return baseUrl + '/feedbacks/csa-chats-distribution'; +}; + +export const getNpsOnSelectedCSAChatsFeedback = (): string => { + return baseUrl + '/feedbacks/agents/nps'; +}; + +export const getDistributionOnSelectedCSAChatsFeedback = (): string => { + return baseUrl + '/feedbacks/agents/distribution'; +}; + +export const getNpsAggregateOnSelectedCSAChatsFeedback = (): string => { + return baseUrl + '/feedbacks/agents/nps-aggregate'; +}; + +export const getNegativeFeedbackChats = (): string => { + return baseUrl + '/feedbacks/negative'; +}; + +// Advisors + +export const getChatForwards = (): string => { + return baseUrl + '/agents/chats/forwards'; +}; + +export const getAvgPickTime = (): string => { + return baseUrl + '/agents/chats/avg-time-picking-up'; +}; + +export const getAvgCsaPresent = (): string => { + return baseUrl + '/agents/avg-active'; +}; + +export const getCsaChatsTotal = (): string => { + return baseUrl + '/agents/chats/total'; +}; + +export const getCsaAvgChatTime = (): string => { + return baseUrl + '/agents/chats/avg-time'; +}; + +// Chats + +export const getTotalChats = (): string => { + return baseUrl + '/chats/total-count'; +}; + +export const getCipChats = (): string => { + return baseUrl + '/chats/contact-information-fulfilled'; +}; + +export const getAvgChatWaitingTime = (): string => { + return baseUrl + '/chats/avg-median-waiting-time'; +}; + +export const getAvgMessagesInChats = (): string => { + return baseUrl + '/chats/avg-num-of-messages'; +}; + +export const getDurationChats = (): string => { + return baseUrl + '/chats/avg-duration'; +}; + +export const getIdleChats = (): string => { + return baseUrl + '/chats/idle-count'; +}; + +export const getBykEndedChats = (): string => { + return baseUrl + '/chats/byk-ended-count'; +}; diff --git a/GUI/src/types/chart.ts b/GUI/src/types/chart.ts index 27b9ffb3..8454a478 100644 --- a/GUI/src/types/chart.ts +++ b/GUI/src/types/chart.ts @@ -6,11 +6,15 @@ export type ChartType = { }; export type ChartData = { - chartData: Record[]; + chartData: Record[]; colors: { id: string; color: string }[]; minPointSize?: MinPointSize; periodNps?: number; periodNpsByCsa?: Record; distributionData?: ChartData; feedBackData?: ChartData; + isRatingDistribution?: boolean; + totalChats?: number; + totalFeedback?: number; + isFiveScale?: boolean; };