Files
libredesk/internal/report/queries.sql
T
Abhinav Raut aa9e4cdae2 feat: add conversation status categories and update related logic.
- Introduced a new category field for conversation statuses to classify them as 'open', 'waiting', or 'resolved'. Which allows max assignment limits in team to work with custom statuses as well.
2026-04-21 16:55:09 +05:30

363 lines
9.6 KiB
SQL

-- name: get-overview-counts
SELECT
json_build_object(
'open',
COUNT(*),
'awaiting_response',
COUNT(
CASE
WHEN c.last_message_sender = 'contact' THEN 1
END
),
'unassigned',
COUNT(
CASE
WHEN c.assigned_user_id IS NULL THEN 1
END
),
'pending',
COUNT(
CASE
WHEN c.first_reply_at IS NULL THEN 1
END
),
'agents_online',
(
SELECT
COUNT(*)
FROM
users
WHERE
availability_status = 'online'
AND type = 'agent'
AND deleted_at is null
),
'agents_away',
(
SELECT
COUNT(*)
FROM
users
WHERE
availability_status = 'away_manual'
AND type = 'agent'
AND deleted_at is null
),
'agents_reassigning',
(
SELECT
COUNT(*)
FROM
users
WHERE
availability_status = 'away_and_reassigning'
AND type = 'agent'
AND deleted_at is null
),
'agents_offline',
(
SELECT
COUNT(*)
FROM
users
WHERE
availability_status = 'offline'
AND type = 'agent'
AND deleted_at is null
)
)
FROM
conversations c
INNER JOIN conversation_statuses s ON c.status_id = s.id
WHERE
s.category != 'resolved';
-- name: get-overview-sla-counts
WITH first_and_resolution AS (
SELECT
COUNT(*) FILTER (
WHERE
first_response_met_at IS NOT NULL
) AS first_response_met_count,
COUNT(*) FILTER (
WHERE
first_response_breached_at IS NOT NULL
) AS first_response_breached_count,
COUNT(*) FILTER (
WHERE
resolution_met_at IS NOT NULL
) AS resolution_met_count,
COUNT(*) FILTER (
WHERE
resolution_breached_at IS NOT NULL
) AS resolution_breached_count,
COALESCE(
AVG(
EXTRACT(
EPOCH
FROM
(first_response_met_at - created_at)
)
) FILTER (
WHERE
first_response_met_at IS NOT NULL
),
0
) AS avg_first_response_time_sec,
COALESCE(
AVG(
EXTRACT(
EPOCH
FROM
(resolution_met_at - created_at)
)
) FILTER (
WHERE
resolution_met_at IS NOT NULL
),
0
) AS avg_resolution_time_sec
FROM
applied_slas
WHERE
created_at >= CASE
WHEN %d = 0 THEN CURRENT_DATE
ELSE NOW() - INTERVAL '%d days'
END
),
next_response AS (
SELECT
COUNT(*) FILTER (
WHERE
met_at IS NOT NULL
) AS next_response_met_count,
COUNT(*) FILTER (
WHERE
breached_at IS NOT NULL
) AS next_response_breached_count,
COALESCE(
AVG(
EXTRACT(
EPOCH
FROM
(met_at - created_at)
)
) FILTER (
WHERE
met_at IS NOT NULL
),
0
) AS avg_next_response_time_sec
FROM
sla_events
WHERE
created_at >= CASE
WHEN %d = 0 THEN CURRENT_DATE
ELSE NOW() - INTERVAL '%d days'
END
AND type = 'next_response'
)
SELECT
fas.first_response_met_count,
fas.first_response_breached_count,
fas.avg_first_response_time_sec,
nr.next_response_met_count,
nr.next_response_breached_count,
nr.avg_next_response_time_sec,
fas.resolution_met_count,
fas.resolution_breached_count,
fas.avg_resolution_time_sec,
CASE
WHEN (fas.first_response_met_count + fas.first_response_breached_count) > 0
THEN ROUND((fas.first_response_met_count::numeric / (fas.first_response_met_count + fas.first_response_breached_count)::numeric) * 100, 1)
ELSE 0
END AS first_response_compliance_percent,
CASE
WHEN (nr.next_response_met_count + nr.next_response_breached_count) > 0
THEN ROUND((nr.next_response_met_count::numeric / (nr.next_response_met_count + nr.next_response_breached_count)::numeric) * 100, 1)
ELSE 0
END AS next_response_compliance_percent,
CASE
WHEN (fas.resolution_met_count + fas.resolution_breached_count) > 0
THEN ROUND((fas.resolution_met_count::numeric / (fas.resolution_met_count + fas.resolution_breached_count)::numeric) * 100, 1)
ELSE 0
END AS resolution_compliance_percent
FROM
first_and_resolution fas,
next_response nr;
-- name: get-overview-charts
WITH new_conversations AS (
SELECT
json_agg(row_to_json(agg)) AS data
FROM
(
SELECT
TO_CHAR(created_at :: date, 'YYYY-MM-DD') AS date,
COUNT(*) AS count
FROM
conversations c
WHERE
c.created_at >= CASE
WHEN %d = 0 THEN CURRENT_DATE
ELSE NOW() - INTERVAL '%d days'
END
GROUP BY
date
ORDER BY
date
) agg
),
resolved_conversations AS (
SELECT
json_agg(row_to_json(agg)) AS data
FROM
(
SELECT
TO_CHAR(resolved_at :: date, 'YYYY-MM-DD') AS date,
COUNT(*) AS count
FROM
conversations c
WHERE
c.resolved_at IS NOT NULL
AND c.created_at >= CASE
WHEN %d = 0 THEN CURRENT_DATE
ELSE NOW() - INTERVAL '%d days'
END
GROUP BY
date
ORDER BY
date
) agg
)
SELECT
json_build_object(
'new_conversations',
(
SELECT
data
FROM
new_conversations
),
'resolved_conversations',
(
SELECT
data
FROM
resolved_conversations
)
) AS result;
-- name: get-overview-csat
SELECT
json_build_object(
'average_rating',
COALESCE(AVG(rating) FILTER (WHERE rating > 0), 0),
'total_responses',
COUNT(*) FILTER (WHERE rating > 0),
'total_sent',
COUNT(*),
'response_rate',
CASE
WHEN COUNT(*) > 0
THEN ROUND((COUNT(*) FILTER (WHERE rating > 0)::numeric / COUNT(*)::numeric) * 100, 1)
ELSE 0
END
) AS result
FROM
csat_responses
WHERE
created_at >= CASE
WHEN %d = 0 THEN CURRENT_DATE
ELSE NOW() - INTERVAL '%d days'
END;
-- name: get-overview-message-volume
WITH stats AS (
SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE type = 'incoming') AS incoming,
COUNT(*) FILTER (WHERE type = 'outgoing') AS outgoing,
COUNT(DISTINCT conversation_id) AS convos
FROM
conversation_messages
WHERE
type IN ('incoming', 'outgoing')
AND created_at >= CASE
WHEN %d = 0 THEN CURRENT_DATE
ELSE NOW() - INTERVAL '%d days'
END
)
SELECT
json_build_object(
'total_messages', total,
'incoming_messages', incoming,
'outgoing_messages', outgoing,
'messages_per_conversation',
CASE
WHEN convos > 0 THEN ROUND(total::numeric / convos::numeric, 1)
ELSE 0
END
) AS result
FROM
stats;
-- name: get-overview-tag-distribution
WITH tag_counts AS (
SELECT
t.id AS tag_id,
t.name AS tag_name,
COUNT(ct.conversation_id) AS count
FROM
tags t
LEFT JOIN conversation_tags ct ON t.id = ct.tag_id
LEFT JOIN conversations c ON ct.conversation_id = c.id
WHERE
c.created_at >= CASE
WHEN %d = 0 THEN CURRENT_DATE
ELSE NOW() - INTERVAL '%d days'
END
OR c.id IS NULL
GROUP BY
t.id, t.name
ORDER BY
count DESC
LIMIT 10
),
tagging AS (
SELECT
COUNT(DISTINCT c.id) FILTER (
WHERE EXISTS (
SELECT 1 FROM conversation_tags ct
WHERE ct.conversation_id = c.id
)
) AS tagged,
COUNT(DISTINCT c.id) FILTER (
WHERE NOT EXISTS (
SELECT 1 FROM conversation_tags ct
WHERE ct.conversation_id = c.id
)
) AS untagged
FROM
conversations c
WHERE
c.created_at >= CASE
WHEN %d = 0 THEN CURRENT_DATE
ELSE NOW() - INTERVAL '%d days'
END
)
SELECT
json_build_object(
'top_tags',
COALESCE((SELECT json_agg(row_to_json(tc)) FROM tag_counts tc), '[]'::json),
'tagged_conversations', tagged,
'untagged_conversations', untagged,
'tagged_percentage',
CASE
WHEN (tagged + untagged) > 0
THEN ROUND((tagged::numeric / (tagged + untagged)::numeric) * 100, 1)
ELSE 0
END
) AS result
FROM
tagging;