mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-10-25 22:09:36 +00:00
7c9fac4f4a
Idle agents still get auto-assigned tickets. Only the manual away statuses skip them. The amber away dot made other agents think the idle person would not get tickets. The agent picker, widget header and the agents report now treat idle as online.
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 IN ('online', 'away')
|
|
) 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 = 'offline'
|
|
) 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; |