supersam/supabase/admin_comms_funnel.sql

150 lines
8.9 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- admin_comms_funnel — воронка коммуникаций для страницы «Аналитика» (mega_admin/admin).
-- Один вызов отдаёт всё, что нужно блоку «Воронка коммуникаций»:
-- stages — этапы цепочки по когорте групп за период (push → SMS → звонки → согласование → ручная → выдача);
-- attempts — с какой попытки берут трубку и согласовывают;
-- channels — чем подтвердили (робот / менеджер / ссылка);
-- lists — «не поднимают трубку» и «открыли ссылку, но не подтвердили» (по 10 записей, для работы менеджера);
-- silent — сколько не достали ни ссылкой, ни звонком.
-- Конвенция периода — как у admin_automation_economics: p_days=30 → группы, созданные за 30 дней;
-- p_days=0 → всё время; p_date_from/p_date_to — явное окно (перекрывает p_days).
create or replace function public.admin_comms_funnel(
p_days integer default 30,
p_date_from timestamp with time zone default null,
p_date_to timestamp with time zone default null
) returns jsonb
language sql
stable
security definer
set search_path to 'public', 'extensions'
as $function$
with cohort as (
select og.*
from order_groups og
where og.created_at >= coalesce(
p_date_from,
case when p_days = 0 then '-infinity'::timestamptz
else now() - make_interval(days => p_days) end)
and og.created_at <= coalesce(p_date_to, now())
),
calls as (
select l.* from autocall_log l join cohort c on c.id = l.order_group_id
),
inv as (
select i.* from delivery_invitations i join cohort c on c.id = i.order_group_id
),
-- группы, которых не достали ни ссылкой, ни ответом на звонок
silent as (
select c.id
from cohort c
where c.notification_status not in ('confirmed', 'opt_out')
and not exists (select 1 from inv i where i.order_group_id = c.id and i.opened_at is not null)
and not exists (select 1 from calls l
where l.order_group_id = c.id
and l.outcome in ('confirmed', 'no_input_day', 'no_input_half'))
),
stages as (
select
(select count(*) from cohort) as groups_cnt,
(select count(*) from cohort c where exists (select 1 from push_log p where p.order_group_id = c.id)) as push_cnt,
(select count(*) from cohort c where c.first_sms_sent_at is not null) as sms1_cnt,
(select count(*) from cohort c where c.second_sms_sent_at is not null) as sms2_cnt,
(select count(*) from inv i where i.opened_at is not null) as opened_cnt,
(select count(*) from cohort c where exists (select 1 from calls l where l.order_group_id = c.id)) as called_cnt,
(select count(*) from cohort c where exists (
select 1 from calls l where l.order_group_id = c.id
and l.outcome in ('confirmed', 'no_input_day', 'no_input_half'))) as answered_cnt,
(select count(*) from cohort c where c.notification_status = 'confirmed') as confirmed_cnt,
(select count(*) from cohort c where c.notification_status = 'manual_required') as manual_cnt,
(select count(*) from cohort c where c.delivery_status in ('delivered', 'picked_up')) as done_cnt,
(select count(*) from silent) as silent_cnt
),
pct as (
select
case when groups_cnt > 0 then round(push_cnt::numeric / groups_cnt * 100, 1) else 0 end as push_pct,
case when groups_cnt > 0 then round(sms1_cnt::numeric / groups_cnt * 100, 1) else 0 end as sms1_pct,
case when groups_cnt > 0 then round(opened_cnt::numeric / groups_cnt * 100, 1) else 0 end as opened_pct,
case when groups_cnt > 0 then round(called_cnt::numeric / groups_cnt * 100, 1) else 0 end as called_pct,
case when groups_cnt > 0 then round(answered_cnt::numeric / groups_cnt * 100, 1) else 0 end as answered_pct,
case when groups_cnt > 0 then round(confirmed_cnt::numeric / groups_cnt * 100, 1) else 0 end as confirmed_pct,
case when groups_cnt > 0 then round(manual_cnt::numeric / groups_cnt * 100, 1) else 0 end as manual_pct,
case when groups_cnt > 0 then round(silent_cnt::numeric / groups_cnt * 100, 1) else 0 end as silent_pct
from stages
),
attempt_rows as (
select attempt_no,
count(*) as total,
count(*) filter (where outcome <> 'no_answer') as answered,
count(*) filter (where outcome = 'confirmed') as confirmed,
count(*) filter (where outcome = 'no_answer') as no_answer
from calls
group by attempt_no
order by attempt_no
),
channel_rows as (
select coalesce(
c.delivery_date_source,
case when exists (select 1 from calls l where l.order_group_id = c.id and l.changed)
then 'autocall' end,
'link'
) as source,
count(*) as cnt
from cohort c
where c.notification_status = 'confirmed'
group by 1
),
no_answer_rows as (
select c.customer_phone as phone, coalesce(nullif(c.town, ''), '—') as town,
c.delivery_type as kind, count(*) as attempts,
max(l.created_at) as last_call, c.notification_status as status
from calls l join cohort c on c.id = l.order_group_id
group by 1, 2, 3, c.id, c.notification_status
having count(*) filter (where l.outcome <> 'no_answer') = 0
order by attempts desc, max(l.created_at) desc
limit 10
),
opened_rows as (
select i.order_number as num, coalesce(nullif(i.customer_name, ''), '—') as name,
i.customer_phone as phone, i.opened_at as opened, i.access_count as opens
from inv i
where i.opened_at is not null and i.confirmed_at is null
order by i.opened_at desc
limit 10
)
select jsonb_build_object(
'period', jsonb_build_object('days', p_days, 'from', p_date_from, 'to', p_date_to),
'stages', jsonb_build_array(
jsonb_build_object('key', 'groups', 'label', 'Групп в работе', 'value', s.groups_cnt, 'pct', 100),
jsonb_build_object('key', 'push', 'label', 'Push отправлен', 'value', s.push_cnt, 'pct', p.push_pct),
jsonb_build_object('key', 'sms1', 'label', 'Первое SMS', 'value', s.sms1_cnt, 'pct', p.sms1_pct),
jsonb_build_object('key', 'opened', 'label', 'Открыли ссылку', 'value', s.opened_cnt, 'pct', p.opened_pct),
jsonb_build_object('key', 'called', 'label', 'Робот позвонил', 'value', s.called_cnt, 'pct', p.called_pct),
jsonb_build_object('key', 'answered', 'label', 'Взяли трубку', 'value', s.answered_cnt, 'pct', p.answered_pct),
jsonb_build_object('key', 'confirmed', 'label', 'Согласовано', 'value', s.confirmed_cnt,'pct', p.confirmed_pct),
jsonb_build_object('key', 'manual', 'label', 'В ручную работу', 'value', s.manual_cnt, 'pct', p.manual_pct),
jsonb_build_object('key', 'done', 'label', 'Выдано/доставлено','value', s.done_cnt, 'pct', case when s.groups_cnt > 0 then round(s.done_cnt::numeric / s.groups_cnt * 100, 1) else 0 end)
),
'silent', jsonb_build_object('count', s.silent_cnt, 'pct', p.silent_pct),
'push_subs', (select count(*) from push_subscriptions),
'second_sms', jsonb_build_object('count', s.sms2_cnt,
'pct', case when s.groups_cnt > 0 then round(s.sms2_cnt::numeric / s.groups_cnt * 100, 1) else 0 end),
'attempts', coalesce((select jsonb_agg(jsonb_build_object(
'attempt', a.attempt_no, 'total', a.total, 'answered', a.answered,
'confirmed', a.confirmed, 'no_answer', a.no_answer,
'answered_pct', case when a.total > 0 then round(a.answered::numeric / a.total * 100, 1) else 0 end,
'confirmed_pct', case when a.total > 0 then round(a.confirmed::numeric / a.total * 100, 1) else 0 end
) order by a.attempt_no) from attempt_rows a), '[]'::jsonb),
'channels', coalesce((select jsonb_agg(jsonb_build_object('source', ch.source, 'count', ch.cnt)
order by ch.cnt desc) from channel_rows ch), '[]'::jsonb),
'no_answer_list', coalesce((select jsonb_agg(jsonb_build_object(
'phone', n.phone, 'town', n.town, 'kind', n.kind, 'attempts', n.attempts,
'last_call', n.last_call, 'status', n.status)) from no_answer_rows n), '[]'::jsonb),
'opened_no_confirm', coalesce((select jsonb_agg(jsonb_build_object(
'num', o.num, 'name', o.name, 'phone', o.phone,
'opened', o.opened, 'opens', o.opens)) from opened_rows o), '[]'::jsonb)
)
from stages s, pct p;
$function$;
comment on function public.admin_comms_funnel(integer, timestamp with time zone, timestamp with time zone) is
'Воронка коммуникаций для аналитики: этапы push → SMS → ссылка → звонки → согласование → ручная работа → выдача.';