-- 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 ), path as ( select count(*) as created, count(*) filter (where first_sms_sent_at is not null) as sms, count(*) filter (where delivery_date is not null or pickup_date is not null) as dated, count(*) filter (where (delivery_date is not null or pickup_date is not null) and delivery_status in ('delivered', 'picked_up')) as chain_done, count(*) filter (where delivery_status in ('delivered', 'picked_up') and delivery_date is null and pickup_date is null) as bypass_done, count(*) filter (where delivery_status = 'paid_storage') as paid_storage, count(*) filter (where delivery_status = 'cancelled') as cancelled, count(*) filter (where delivery_type = 'pickup') as pickup_cnt, count(*) filter (where delivery_type = 'delivery') as delivery_cnt from cohort ), path_channels as ( select case when c.delivery_date_source = 'autocall' then 'call' when c.delivery_date_source = 'manual' then 'manual' when c.delivery_date_source = 'push' then 'push' when c.delivery_date_source in ('sms', 'client', 'messenger') then 'sms' when exists (select 1 from delivery_invitations di where di.order_group_id = c.id and di.confirmed_at is not null) then 'sms' when c.manual_confirmation_at is not null then 'manual' when c.called then 'call' else 'unknown' end as channel, count(*) as cnt from cohort c where c.delivery_date is not null or c.pickup_date is not null group by 1 ), path_sources as ( select coalesce(delivery_date_source, 'none') as source, count(*) as cnt from cohort where delivery_date is not null or pickup_date is not null group by 1 ), 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), -- Путь заказа: воронка по цепочке (без даты водитель не едет) + то, что прошло мимо неё. 'order_path', jsonb_build_object( 'stages', jsonb_build_array( jsonb_build_object('key', 'created', 'label', 'Групп в работе', 'value', pp.created, 'pct', 100), jsonb_build_object('key', 'sms', 'label', 'Первое SMS', 'value', pp.sms, 'pct', case when pp.created > 0 then round(pp.sms::numeric / pp.created * 100, 1) else 0 end), jsonb_build_object('key', 'dated', 'label', 'Дата согласована', 'value', pp.dated, 'pct', case when pp.created > 0 then round(pp.dated::numeric / pp.created * 100, 1) else 0 end), jsonb_build_object('key', 'done', 'label', 'Выдано по цепочке', 'value', pp.chain_done, 'pct', case when pp.created > 0 then round(pp.chain_done::numeric / pp.created * 100, 1) else 0 end) ), 'bypass', jsonb_build_object('delivered', pp.bypass_done, 'paid_storage', pp.paid_storage, 'cancelled', pp.cancelled), 'split', jsonb_build_object('pickup', pp.pickup_cnt, 'delivery', pp.delivery_cnt), 'sources', coalesce((select jsonb_agg(jsonb_build_object('source', ps.source, 'count', ps.cnt) order by ps.cnt desc) from path_sources ps), '[]'::jsonb), -- Каким каналом согласовали дату: push / sms / звонок робота / вручную (логист). 'channels', coalesce((select jsonb_agg(jsonb_build_object('channel', pc.channel, 'count', pc.cnt) order by pc.cnt desc) from path_channels pc), '[]'::jsonb) ) ) from stages s, pct p, path pp; $function$; comment on function public.admin_comms_funnel(integer, timestamp with time zone, timestamp with time zone) is 'Воронка коммуникаций для аналитики: этапы push → SMS → ссылка → звонки → согласование → ручная работа → выдача.';