mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-10 14:15:42 +00:00
364 lines
10 KiB
SQL
364 lines
10 KiB
SQL
-- name: get-overview-counts
|
|
WITH convs AS (
|
|
SELECT
|
|
COUNT(*) AS open,
|
|
COUNT(*) FILTER (
|
|
WHERE
|
|
c.last_message_sender = 'contact'
|
|
) AS awaiting_response,
|
|
COUNT(*) FILTER (
|
|
WHERE
|
|
c.assigned_user_id IS NULL
|
|
) AS unassigned,
|
|
COUNT(*) FILTER (
|
|
WHERE
|
|
c.first_reply_at IS NULL
|
|
) AS pending
|
|
FROM
|
|
conversations c
|
|
INNER JOIN conversation_statuses s ON c.status_id = s.id
|
|
WHERE
|
|
s.category != 'resolved'
|
|
),
|
|
agents AS (
|
|
SELECT
|
|
COUNT(*) FILTER (
|
|
WHERE
|
|
availability_status = 'online'
|
|
) AS agents_online,
|
|
COUNT(*) FILTER (
|
|
WHERE
|
|
availability_status = 'away_manual'
|
|
) AS agents_away,
|
|
COUNT(*) FILTER (
|
|
WHERE
|
|
availability_status = 'away_and_reassigning'
|
|
) AS agents_reassigning,
|
|
COUNT(*) FILTER (
|
|
WHERE
|
|
availability_status IN ('offline', 'away')
|
|
) AS agents_offline
|
|
FROM
|
|
users
|
|
WHERE
|
|
type = 'agent'
|
|
AND deleted_at IS NULL
|
|
)
|
|
SELECT
|
|
json_build_object(
|
|
'open', open,
|
|
'awaiting_response', awaiting_response,
|
|
'unassigned', unassigned,
|
|
'pending', pending,
|
|
'agents_online', agents_online,
|
|
'agents_away', agents_away,
|
|
'agents_reassigning', agents_reassigning,
|
|
'agents_offline', agents_offline
|
|
)
|
|
FROM
|
|
convs,
|
|
agents;
|
|
|
|
-- name: get-overview-sla-counts
|
|
-- Count only each conversation's latest applied SLA; superseded rows are kept as history and would double-count.
|
|
WITH latest_applied AS (
|
|
SELECT DISTINCT ON (conversation_id)
|
|
created_at, first_response_met_at, first_response_breached_at,
|
|
resolution_met_at, resolution_breached_at
|
|
FROM applied_slas
|
|
WHERE created_at >= CASE
|
|
WHEN %d = 0 THEN CURRENT_DATE
|
|
ELSE NOW() - INTERVAL '%d days'
|
|
END
|
|
ORDER BY conversation_id, created_at DESC, id DESC
|
|
),
|
|
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
|
|
latest_applied
|
|
),
|
|
next_response AS (
|
|
-- A reply after the deadline carries both met_at and breached_at, so counting the
|
|
-- timestamps puts one event in both buckets. status holds a single terminal verdict.
|
|
SELECT
|
|
COUNT(*) FILTER (
|
|
WHERE
|
|
status = 'met'
|
|
) AS next_response_met_count,
|
|
COUNT(*) FILTER (
|
|
WHERE
|
|
status = 'breached'
|
|
) AS next_response_breached_count,
|
|
COALESCE(
|
|
AVG(
|
|
EXTRACT(
|
|
EPOCH
|
|
FROM
|
|
(met_at - created_at)
|
|
)
|
|
) FILTER (
|
|
WHERE
|
|
status = 'met'
|
|
),
|
|
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 >= 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 per_conversation AS (
|
|
SELECT
|
|
conversation_id,
|
|
COUNT(*) AS total,
|
|
COUNT(*) FILTER (WHERE type = 'incoming') AS incoming,
|
|
COUNT(*) FILTER (WHERE type = 'outgoing') AS outgoing
|
|
FROM
|
|
conversation_messages
|
|
WHERE
|
|
type IN ('incoming', 'outgoing')
|
|
AND private = false
|
|
AND (type = 'incoming' OR status = 'sent')
|
|
AND created_at >= CASE
|
|
WHEN %d = 0 THEN CURRENT_DATE
|
|
ELSE NOW() - INTERVAL '%d days'
|
|
END
|
|
GROUP BY
|
|
conversation_id
|
|
),
|
|
stats AS (
|
|
SELECT
|
|
COALESCE(SUM(total), 0) AS total,
|
|
COALESCE(SUM(incoming), 0) AS incoming,
|
|
COALESCE(SUM(outgoing), 0) AS outgoing,
|
|
COUNT(*) AS convos
|
|
FROM
|
|
per_conversation
|
|
)
|
|
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(c.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
|
|
AND c.created_at >= CASE
|
|
WHEN %d = 0 THEN CURRENT_DATE
|
|
ELSE NOW() - INTERVAL '%d days'
|
|
END
|
|
GROUP BY
|
|
t.id, t.name
|
|
ORDER BY
|
|
count DESC, t.id
|
|
LIMIT 10
|
|
),
|
|
tagging AS (
|
|
SELECT
|
|
COUNT(*) AS total,
|
|
COUNT(*) FILTER (
|
|
WHERE EXISTS (
|
|
SELECT 1 FROM conversation_tags ct
|
|
WHERE ct.conversation_id = c.id
|
|
)
|
|
) AS tagged
|
|
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', total - tagged,
|
|
'tagged_percentage',
|
|
CASE
|
|
WHEN total > 0
|
|
THEN ROUND((tagged::numeric / total::numeric) * 100, 1)
|
|
ELSE 0
|
|
END
|
|
) AS result
|
|
FROM
|
|
tagging; |