-- 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 → ссылка → звонки → согласование → ручная работа → выдача.';