414 lines
28 KiB
Markdown
414 lines
28 KiB
Markdown
# Автодозвон SuperSam — детальный план реализации (БД → API → планировщик → UI)
|
||
|
||
> **Дата:** 01.09.2026 · **Статус АТС:** диалоговая машина и голос Vivienne проверены живьём.
|
||
> **Контракт реализации:** каждая фаза — отдельный PR, сначала dev (dev.mkn8n.ru), prod — только после проверки Михаилом. Mobile-first: реестр и журнал — подписанные строки на viewport 390px, таблица только desktop. Термины/строки в UI и SQL — ровно те, что уже используются в приложении («Первая половина дня» и т.п.).
|
||
|
||
---
|
||
|
||
## Фаза 0 — Заморозка контрактов (нулевой код, только согласование)
|
||
|
||
**Цель:** зафиксировать значения, вокруг которых всё строится, чтобы не разъехались UI, БД и AGI.
|
||
|
||
**Файл:** `docs/autocall-contract.md` (новый, в репо).
|
||
|
||
Содержимое:
|
||
1. **Кампания-ключ:** `campaign_type='autocall_slot'` (первый сценарий). Резерв ключей: `autocall_storage`, `autocall_reminder`, `autocall_confirm_morning`.
|
||
2. **Сценарии звонка:** `A` (день+время), `B` (время, день=завтра), `C` (подтверждение выбранного, молчание=да).
|
||
3. **Outcome-значения `autocall_log.outcome` (единственный enum, на него смотрит UI):**
|
||
`confirmed`, `confirmed_kept_previous`, `confirmed_silent`, `no_answer`, `no_input_day`, `no_input_half`, `operator`, `opt_out`, `invalid_number`, `error`.
|
||
4. **DTMF-семантика:** `dtmf_day`: 1=завтра, 2=послезавтра; `dtmf_half`: 1=первая половина дня, 2=вторая. Больше ничего. 0/9/* — не сохраняются как ответ, а формируют outcome.
|
||
5. **Записи в `order_groups` при `confirmed*`:** `delivery_date/pickup_date`, `delivery_time/pickup_time_slot`, `delivery_date_source='autocall'`, `delivery_status='agreed'`, `notification_status='confirmed'`, `called=true`, `called_at`, `called_comment='Согласовано роботом: …'`.
|
||
6. **Эскалация:** `operator`, `opt_out`, `invalid_number`, `no_input×no_input_escalate_after`, `attempts≥max_attempts` → `notification_status='manual_required'` (плюс `autocall_opt_out=true` только для 9).
|
||
7. **Тайминг:** отсчёт от `sms_sent_at`, пауза `delay_after_sms_minutes` (90), `retry_interval_minutes` (120), попытки в день `attempts_per_day` (3), следующий день `next_day_attempt_times` (`10:00,14:00`), окно `work_hours_start/end` 9–20, `work_days` пн–сб. Все — из БД, не хардкод.
|
||
8. **Тест:** `test_mode=true` → набор `test_phone`; `was_test_mode=true` пишем в лог.
|
||
|
||
**Проверка:** файл открыт и подтверждён Михаилом в PR-описании.
|
||
|
||
---
|
||
|
||
## Фаза 1 — База данных (миграция dev, потом prod)
|
||
|
||
### Файлы
|
||
- Создать `supabase/migrations/20260901_autocall_base.sql` (модель: один файл, применяем как остальные DDL — см. правило: DDL только `psql -U supabase_admin`).
|
||
|
||
### Что делает
|
||
```sql
|
||
-- 1) лог звонков
|
||
create table if not exists public.autocall_log (
|
||
id bigint generated always as identity primary key,
|
||
order_group_id uuid references public.order_groups(id) on delete set null,
|
||
campaign_type text not null default 'autocall_slot',
|
||
customer_phone text not null,
|
||
attempt_no smallint not null,
|
||
scenario text not null check (scenario in ('A','B','C')),
|
||
call_started_at timestamptz not null,
|
||
answered_at timestamptz,
|
||
duration_sec smallint,
|
||
sip_code smallint,
|
||
channel_state text,
|
||
dtmf_day smallint check (dtmf_day in (1,2) or dtmf_day is null),
|
||
dtmf_half smallint check (dtmf_half in (1,2) or dtmf_half is null),
|
||
changed boolean not null default false,
|
||
outcome text not null check (outcome in (
|
||
'confirmed','confirmed_kept_previous','confirmed_silent',
|
||
'no_answer','no_input_day','no_input_half',
|
||
'operator','opt_out','invalid_number','error')),
|
||
was_test_mode boolean not null default false,
|
||
asterisk_uniqueid text,
|
||
created_at timestamptz not null default now()
|
||
);
|
||
create index if not exists idx_autocall_log_group on public.autocall_log(order_group_id, created_at desc);
|
||
create index if not exists idx_autocall_log_phone on public.autocall_log(customer_phone, created_at desc);
|
||
create index if not exists idx_autocall_log_outcome on public.autocall_log(outcome, created_at desc);
|
||
create index if not exists idx_autocall_log_uniq on public.autocall_log(asterisk_uniqueid);
|
||
comment on table public.autocall_log is 'Лог робо-звонков (автодозвон через АТС клиента). UI читает outcome.';
|
||
grant select on public.autocall_log to authenticated;
|
||
|
||
-- 2) order_groups
|
||
alter table public.order_groups add column if not exists autocall_opt_out boolean not null default false;
|
||
create index if not exists idx_order_groups_autocall_optout
|
||
on public.order_groups(autocall_opt_out) where autocall_opt_out = true;
|
||
|
||
-- 3) новые колонки настроек (если их ещё нет)
|
||
alter table public.sms_campaign_settings add column if not exists delay_after_sms_minutes int;
|
||
alter table public.sms_campaign_settings add column if not exists retry_interval_minutes int;
|
||
alter table public.sms_campaign_settings add column if not exists attempts_per_day int;
|
||
alter table public.sms_campaign_settings add column if not exists no_input_escalate_after int;
|
||
alter table public.sms_campaign_settings add column if not exists next_day_attempt_times text;
|
||
alter table public.sms_campaign_settings add column if not exists call_timeout_seconds int;
|
||
|
||
-- 4) строка кампании (в боевом режиме — выключена и тестовая)
|
||
insert into public.sms_campaign_settings
|
||
(campaign_type, enabled, test_mode, test_phone, work_hours_start, work_hours_end, work_days,
|
||
max_attempts, delay_after_sms_minutes, retry_interval_minutes, attempts_per_day,
|
||
no_input_escalate_after, next_day_attempt_times, call_timeout_seconds)
|
||
values
|
||
('autocall_slot', false, true, '9067029953', 9, 20, '1,2,3,4,5,6',
|
||
5, 90, 120, 3,
|
||
2, '10:00,14:00', 30)
|
||
on conflict (campaign_type) do nothing;
|
||
|
||
-- 5) представление флагов для реестра (без N+1)
|
||
create or replace view public.order_group_comms_flags as
|
||
select
|
||
og.id,
|
||
og.autocall_opt_out,
|
||
og.delivery_date_source,
|
||
og.notification_status,
|
||
og.delivery_status,
|
||
(select l.outcome from public.autocall_log l where l.order_group_id = og.id order by l.created_at desc limit 1) as last_call_outcome,
|
||
(select l.changed from public.autocall_log l where l.order_group_id = og.id order by l.created_at desc limit 1) as last_call_changed,
|
||
(select count(*) from public.autocall_log l where l.order_group_id = og.id) as call_attempts,
|
||
(select count(*) from public.autocall_log l where l.order_group_id = og.id
|
||
and l.outcome in ('no_input_day','no_input_half')) as no_input_count
|
||
from public.order_groups og;
|
||
grant select on public.order_group_comms_flags to authenticated;
|
||
```
|
||
|
||
### Применение и проверка
|
||
- Dev: `docker exec supabase-db psql -U supabase_admin -d postgres -f /tmp/20260901_autocall_base.sql`
|
||
- Проверки: `\d autocall_log`; `select campaign_type from sms_campaign_settings where campaign_type='autocall_slot'`; `select * from order_group_comms_flags limit 1`.
|
||
|
||
---
|
||
|
||
## Фаза 2 — Edge function `autocall-result`
|
||
|
||
### Файлы
|
||
- Создать `supabase/functions/autocall-result/index.ts`
|
||
- Общие helpers: `supabase/functions/_shared/` (используем существующие cors/service-role паттерны из `confirm-delivery-choice`).
|
||
|
||
### Контракт endpoint'а
|
||
`POST /functions/v1/autocall-result`
|
||
Headers: `x-webhook-secret: <WEBHOOK_SECRET>` (тот же стиль, что у `dadata-suggest`).
|
||
|
||
Body (JSON ровно в этом виде; AGI шлёт curl'ом):
|
||
```json
|
||
{
|
||
"asterisk_uniqueid":"1788283049.23",
|
||
"order_group_id":"uuid-or-null",
|
||
"campaign_type":"autocall_slot",
|
||
"customer_phone":"79067029953",
|
||
"attempt_no":2,
|
||
"scenario":"A",
|
||
"call_started_at":"2026-09-01T20:17:45+03:00",
|
||
"answered_at":"2026-09-01T20:17:45+03:00",
|
||
"duration_sec":65,
|
||
"sip_code":200,
|
||
"channel_state":"Up",
|
||
"dtmf_day":1,"dtmf_half":2,"changed":true,
|
||
"outcome":"confirmed",
|
||
"was_test_mode":true
|
||
}
|
||
```
|
||
|
||
### Логика функции (по шагам)
|
||
1. Валидация секрета и полей; `outcome` — только из enum (Фаза 0).
|
||
2. Идемпотентность: upsert по `asterisk_uniqueid` (повторный curl не плодит строки).
|
||
3. Вставка в `autocall_log`.
|
||
4. Если `order_group_id` нет — найти последний активный заказ по `customer_phone`
|
||
(`status not in ('closed','cancelled')`, `delivery_status='pending_confirmation'`).
|
||
5. При `confirmed | confirmed_kept_previous | confirmed_silent`:
|
||
- вычислить `delivery_date` = (завтра/послезавтра от даты звонка, по Europe/Moscow);
|
||
- `delivery_time` = `'Первая половина дня'|'Вторая половина дня'` (точно эти строки);
|
||
- pickup: вместо delivery_* пишем `pickup_date`,`pickup_time_slot` при `delivery_type='pickup'`;
|
||
- обновить `order_groups`: поля из Фазы 0 п.5 + `next_notification_check_at=null`.
|
||
6. При `operator|opt_out|invalid_number`: `notification_status='manual_required'`, `called=true`,
|
||
`called_comment=...`; для `opt_out` ещё `autocall_opt_out=true`.
|
||
7. При `no_answer|no_input_day|no_input_half`: посчитать уже сделанные попытки;
|
||
при `attempt_no >= max_attempts` или `no-input >= no_input_escalate_after` → `manual_required`,
|
||
иначе оставить `notification_status='link_ready'` (планировщик назначит следующую).
|
||
8. `auth.uid()` пуст → в `order_history` source проставится «Система» (триггер уже работает);
|
||
добавить `metadata.source='autocall'` через update-поле (если триггер поддерживает; иначе отдельный insert в `order_history`).
|
||
9. Ответ: `{ok:true}` / `{ok:false,error}`.
|
||
|
||
### Тесты (ручные, dev)
|
||
- curl с тестовым uniqueid → строка в `autocall_log`, заказ обновился, в UI бейдж «🤖 слот от робота».
|
||
- Повторный curl с тем же uniqueid → дубликата нет.
|
||
|
||
---
|
||
|
||
## Фаза 3 — Планировщик (systemd на проде, dev — ручной запуск)
|
||
|
||
### Файлы
|
||
- Создать `scripts/autocall_campaign.py`
|
||
- Создать `scripts/autocall_campaign.service`
|
||
- Создать `scripts/autocall_campaign.timer`
|
||
- Конфиг-реюз: импортировать helper'ы из `sms_run_with_db.sh` (резолв IP supabase-db).
|
||
|
||
### Алгоритм тика (запуск каждые 10 мин)
|
||
1. Загрузить настройки `autocall_slot` из `sms_campaign_settings`. Если `enabled=false` → выход.
|
||
2. Выбор кандидатов:
|
||
```sql
|
||
select id, customer_phone, delivery_type, first_sms_sent_at
|
||
from order_groups
|
||
where notification_status='link_ready'
|
||
and delivery_status='pending_confirmation'
|
||
and autocall_opt_out=false
|
||
and first_sms_sent_at is not null
|
||
and first_sms_sent_at <= now() - (delay_after_sms_minutes || ' minutes')::interval
|
||
and coalesce((select max(attempt_no) from autocall_log l where l.order_group_id=order_groups.id),0) < max_attempts
|
||
and (next_notification_check_at is null or next_notification_check_at <= now());
|
||
```
|
||
В test_mode — `customer_phone` заменяем на `test_phone`.
|
||
3. Проверка окна: `work_hours_start <= now < work_hours_end`, день недели ∈ `work_days`; иначе — отложить (`next_notification_check_at` = ближайшее начало окна).
|
||
4. Сценарий: если дата «готовности» = сегодня → `A`, иначе `B`. (C планировщик пока не выбирает — сценарий C включается отдельной задачей позже.)
|
||
5. Расчёт номера попытки: `attempt_no = колв-во строк в autocall_log + 1`; распределение по дню/след.дню: если `attempt_no <= attempts_per_day` → сейчас; иначе по `next_day_attempt_times`.
|
||
6. Генерация call-файла в `/tmp/ss-<group>-<attempt>.call`:
|
||
```
|
||
Channel: PJSIP/0<phone10>@mmedia
|
||
Context: SS-Autocall-Slot
|
||
Extension: s
|
||
Priority: 1
|
||
MaxRetries: 0
|
||
WaitTime: 30
|
||
Archive: yes
|
||
Setvar: SCENARIO=A
|
||
Setvar: CALLTYPE=dlv
|
||
Setvar: GROUP_ID=<uuid>
|
||
Setvar: ATTEMPT=<n>
|
||
Setvar: API_URL=https://<prod>/functions/v1/autocall-result
|
||
Setvar: API_KEY=<webhook-secret>
|
||
Setvar: TEST_MODE=0|1
|
||
```
|
||
7. Передача на АТС: `scp → /tmp` (атомарно mv в `/var/spool/asterisk/outgoing/`), ssh-кред только на сервере (никаких ключей в репо — в `/opt/supersam/.env` локально).
|
||
8. Логирование тика в `sms_campaign_log`-стиле своей строкой `campaign_type='autocall_slot'` (техническая метка «тик был») — опционально, для отладки таймера.
|
||
9. Обработка ручных запросов: таблица `autocall_queue` (НОВАЯ, минимальная):
|
||
```sql
|
||
create table if not exists public.autocall_queue (
|
||
id bigint generated always as identity primary key,
|
||
order_group_id uuid not null,
|
||
phone text not null,
|
||
scenario text not null default 'A',
|
||
requested_by uuid,
|
||
requested_at timestamptz not null default now(),
|
||
processed_at timestamptz
|
||
);
|
||
```
|
||
Кнопка «Позвонить сейчас» пишет сюда; тик забирает `processed_at is null`, создаёт call-файл, ставит `processed_at`. (Так ssh никогда не попадает в edge fn.)
|
||
|
||
### Юнит-проверки (без настоящих звонков)
|
||
- Запустить скрипт с `--dry-run`: в консоль печатает список кандидатов и текст call-файла.
|
||
- `test_mode=true`, `test_phone=<твой>` → один звонок тебе.
|
||
|
||
### Таймер
|
||
```
|
||
# autocall_campaign.timer
|
||
[Timer]
|
||
OnCalendar=*:0/10
|
||
Persistent=true
|
||
|
||
# autocall_campaign.service
|
||
[Service]
|
||
Type=oneshot
|
||
ExecStart=/opt/supersam/scripts/sms_run_with_db.sh /opt/supersam/scripts/autocall_campaign.py
|
||
```
|
||
|
||
---
|
||
|
||
## Фаза 4 — Диалплан и AGI-связь с API (изменения на АТС, с показом Паше)
|
||
|
||
1. `ss-dialog.agi` дополнить: в конце (после определения итогового `OUTCOME`) — curl на `API_URL` с телом из Фазы 2. Переменные приходят из call-файла (`GROUP_ID`, `ATTEMPT`, `TEST_MODE`).
|
||
Покрыть: потерю сети (curl timeout 5s, не валить звонок — результат всё равно в `/tmp/ss-result-*.txt`).
|
||
2. `[SS-Autocall-Slot]` — additive контекст (диф Паше до применения).
|
||
3. `pbx_spool.so` — попросить постоянную загрузку (иначе ночной reboot 03:33 сносит).
|
||
|
||
---
|
||
|
||
## Фаза 5 — UI: оболочка «Коммуникации» (вкладки)
|
||
|
||
### Файлы
|
||
- Создать `src/components/admin/CommunicationsPanel.jsx` — подключает вкладки.
|
||
- Перенести содержимое `SmsCampaignPanel.jsx` в `src/components/admin/sms/SmsChannelTab.jsx` (почти copy-paste, имя файла сохранить в git как rename).
|
||
- `src/pages/DashboardPage.jsx`:
|
||
- импорт `CommunicationsPanel` вместо `SmsCampaignPanel`;
|
||
- `MEGA_ADMIN_NAV`: `{ key:"sms_campaign", label:"Коммуникации", description:"SMS, автодозвон и аналитика обращений." }` (key не трогаем — совместимость `?tab=sms_campaign`).
|
||
- роут: `if (activeSection === "sms_campaign") return <CommunicationsPanel/>`.
|
||
|
||
### Поведение вкладок
|
||
- Состояние: `searchParams.channel` ∈ `sms | autocall | analytics`, дефолт `sms`.
|
||
- UI: таблетки (по стилю существующего период-селектора в `SmsCampaignStats`).
|
||
- На мобильном: вкладки в одну строку с горизонтальным скроллом, подписи короткие.
|
||
|
||
### Проверка (dev)
|
||
- Прямой переход `/dashboard?tab=sms_campaign&channel=autocall` открывает нужную вкладку.
|
||
- Все 4 SMS-карточки, статистика, журнал — без регрессий.
|
||
- На 390px — ничего не ломается.
|
||
|
||
---
|
||
|
||
## Фаза 6 — UI: вкладка «Автодозвон»
|
||
|
||
### Файлы
|
||
- Создать `src/components/admin/autocall/AutocallChannelTab.jsx` — карточки, настройки, журнал.
|
||
- Создать `src/components/admin/autocall/AutocallLogTable.jsx` — таблица/строки + фильтры + поиск.
|
||
- Создать `src/components/admin/autocall/AutocallStats.jsx` — графики (recharts, как SmsCampaignStats).
|
||
|
||
### Карточки кампаний звонков
|
||
`CAMPAIGNS` для канала:
|
||
```js
|
||
[
|
||
{ key:"autocall_slot", label:"Согласование слота", icon:"🔊", live:true },
|
||
{ key:"autocall_storage", label:"Платное хранение (голосом)",icon:"📦", live:false },
|
||
{ key:"autocall_reminder", label:"Напоминание накануне", icon:"⏰", live:false },
|
||
{ key:"autocall_confirm_morning", label:"Подтверждение утром", icon:"☀️", live:false },
|
||
]
|
||
```
|
||
Неработающие — карточка disabled с бейджем «скоро». Рабочая — по шаблону SMS-карточек: тоггл вкл/выкл, бейдж (⏸/🧪/🚀), мини-статистика Всего / Дозвон / Согласовали, кнопки ▶/🧪/🔄.
|
||
|
||
### Настройки `autocall_slot` (форма, все поля из Фазы 0 п.7)
|
||
Группы:
|
||
- Режим: тест/боевой + тестовый номер.
|
||
- Интервалы: `delay_after_sms_minutes`, `retry_interval_minutes`, `attempts_per_day`, `max_attempts`, `next_day_attempt_times`.
|
||
- Эскалация: `no_input_escalate_after`.
|
||
- Окно: часы (селекты как у SMS) и дни-таблетки.
|
||
- Telegram chat ID для алертов недозвона.
|
||
Кнопка «Сохранить» — тот же паттерн (`update sms_campaign_settings`). По умолчанию `enabled=false`, `test_mode=true`.
|
||
|
||
### Журнал звонков
|
||
- Данные: `supabase.from("autocall_log").select("*").order("created_at desc").limit(200)`.
|
||
- Таблица (desktop): `Заказ | Клиент/телефон | Попытка | Время | Длит. | Исход | Слот`.
|
||
- Mobile (<640px): подписанные строки:
|
||
```
|
||
🔊 Володя · +7 906 702-99-53
|
||
Исход: ✅ Согласовал (попытка 2)
|
||
Слот: завтра, первая половина дня
|
||
01.09.26, 20:18
|
||
```
|
||
- Бейджи исходов: тон/иконка/текст из маппинга (Фаза 0 п.3); `confirmed*` — зелёные, `no_answer` — нейтраль, `no_input*` — warning, `operator/opt_out/invalid_number/error` — danger.
|
||
- Фильтры-chips: все + 10 исходов (значение = outcome). Поиск: по номеру заказа/телефону/имени (общий helper с SMS-журналом).
|
||
- Клик по строке → `/dashboard/group/<order_group_id>`.
|
||
- Пагинация 20/стр, автообновление 30с (тоггл, как у SMS).
|
||
|
||
---
|
||
|
||
## Фаза 7 — Бейджи снаружи (реестр и логистика)
|
||
|
||
### Файлы
|
||
- `src/components/logistics/LogisticsReadinessBoard.jsx` — колонка/строка бейджа.
|
||
- `src/hooks/useOrderGroups.js` — подтягивать `order_group_comms_flags` одним запросом join'ом (или вторым запросом по списку id — проще, 1 запрос).
|
||
|
||
### Бейдж (приоритет сверху вниз — один наружу)
|
||
| Условие | Бейдж | Тон |
|
||
|---|---|---|
|
||
| `autocall_opt_out` | 🚫 отказ от звонков | danger |
|
||
| `last_call_outcome='invalid_number'` | ❌ неверный номер | danger |
|
||
| `notification_status='manual_required'` + были звонки | 👤 менеджеру | warning |
|
||
| `no_input_count ≥ 2` | 🤐 не нажимает | warning |
|
||
| `call_attempts ≥ 3` + последний `no_answer` | 📵 не дозвонились | warning |
|
||
| `delivery_date_source='autocall'` + `last_call_changed` | 🤖✏️ слот от робота (правился) | info |
|
||
| `delivery_date_source='autocall'` | 🤖 слот от робота | info |
|
||
|
||
Реализация: компонент `src/components/UI/CallStatusBadge.jsx` (props: флаги), рендерится:
|
||
- desktop — отдельная колонка «Дозвон» в таблице групп;
|
||
- mobile — иконка в подписанной строке (рядом с телефоном).
|
||
|
||
### Фильтры реестра
|
||
Chips над таблицей (мультивыбор): `📵 Не дозвонились`, `🤐 Не нажимают`, `👤 Менеджеру (после робота)`, `🤖 Согласовано роботом`, `🚫 Отказ от звонков`. Фильтрация — на клиенте по уже загруженным флагам (скрываем строки), без новых запросов.
|
||
|
||
### Дашборд логистика и водителя
|
||
В карточках доставок — мелкая пометка «🤖» возле времени слота при `delivery_date_source='autocall'`. Без фильтров, только визуальная пометка.
|
||
|
||
---
|
||
|
||
## Фаза 8 — Карточка заказа: блок «Автодозвон»
|
||
|
||
### Файл
|
||
- Создать `src/components/orders/AutocallStatusCard.jsx`; подключить в `DeliverySetDetailPanel.jsx` рядом с `SmsStatusCard`.
|
||
|
||
### Содержимое
|
||
- Заголовок: «📞 Автодозвон» + бейдж-итог (успех/в работе/менеджеру/отказ).
|
||
- Слот: если `confirmed*` — «Завтра, первая половина дня» (плюс пометка «согласовано роботом»).
|
||
- История: до 5 строк из `autocall_log` этого заказа (иконка исхода, время, попытка).
|
||
- Если `last_call_changed=true` — подсказка: «Клиент правил выбор. При сомнении — перезвоните».
|
||
- Кнопки (роли `mega_admin`, `logistician`):
|
||
- `[📞 Позвонить сейчас]` → insert в `autocall_queue` (см. Фаза 3, п.9), кнопка заменяется на «В очереди ✓».
|
||
- `[🚫 Не звонить]` / `[🔔 Вернуть в автодозвон]` → toggle `autocall_opt_out`.
|
||
- В `order_history` уже попадает (триггер); доработка: `metadata.source='autocall'` — вывести в таймлайне как «Автодозвон», если UI таймлайна отображает source.
|
||
|
||
---
|
||
|
||
## Фаза 9 — Вкладка «Аналитика»
|
||
|
||
### Файл
|
||
- Создать `src/components/admin/analytics/CommsAnalyticsTab.jsx`.
|
||
|
||
### Блоки
|
||
1. **Сводка каналов за период** (7/14/30/всё — селектор как в SmsCampaignStats):
|
||
- SMS: отправлено/доставлено/конверсия переходов по ссылке (если считаем — иначе «н/д»),
|
||
- Звонки: попыток/дозвонились/согласовали/эскалации,
|
||
- Итог: «Согласовано автоматикой» vs `manual_required` всего.
|
||
2. **Воронка автодозвона** (bar): SMS→звонок1→…→звонок5→согласовано/менеджеру.
|
||
3. **Исходы** (donut из `autocall_log.outcome`).
|
||
4. **По дням** (bar: попыток/дозвон/согласовано) + **конверсия согласования** (line).
|
||
5. **«Согласовали с N-й попытки»** (bar) — покажет, нужна ли 4–5 попытка.
|
||
6. **Минуты**: «время на линии всего / средний звонок» (sum/avg `duration_sec`) — данные для разговора с Пашей о тарифе Miranda.
|
||
7. **Экспорт CSV** текущего фильтра журнала звонков и сводки (кнопка «⬇ CSV», генерим на клиенте Blob).
|
||
|
||
Все запросы — только чтение `autocall_log`/`sms_campaign_log` за период; без новых таблиц.
|
||
|
||
---
|
||
|
||
## Фаза 10 — Раскатка и приёмка
|
||
|
||
1. **Dev прогон**: миграция → edge fn → планировщик (dry-run) → UI (вкладки, журнал, бейджи, карточка, аналитика) на 390px и desktop.
|
||
2. **Тестовый заказ**: создать заказ, прогнать цикл SMS → (не выбирает слот) → звонок на `test_phone` → согласование → проверить карточку, бейдж в реестре, таймлайн, аналитику.
|
||
3. **Боевая проба на твоём номере**: `test_mode=true`, реальный заказ → ты нажимаешь 1/2 → всё сошлось.
|
||
4. **Перенос prod**: миграция, edge fn, планировщик (таймер включён, кампания `enabled=false`), UI-деплой (бамп SW cache).
|
||
5. **Включение prod** — только по твоей команде: сначала `test_mode=true` (боевой prod, но звонит тебе), потом выключаем test_mode.
|
||
6. **Гипигиена**: вернуть `approvals` в manual, секреты — в `/opt/supersam/.env`, доки: `docs/autocall-contract.md` актуален.
|
||
|
||
---
|
||
|
||
## Риски и вопросы (держим перед глазами)
|
||
- Reboot АТС 03:33 → Паша ставит `pbx_spool.so` в автозагрузку (диф показан до).
|
||
- Тариф Miranda не считается из CDR → своя статистика минут (Фаза 9, п.6).
|
||
- Отказ от звонков — юр. требование: озвучка «9» уже в диалоге; счётчик opt-out на карточке кампании.
|
||
- Никаких ssh из edge fn — ручные звонки только через `autocall_queue` + планировщик.
|
||
- Совместимость URL: `?tab=sms_campaign` сохранён как key; label меняем на «Коммуникации».
|
||
|
||
## Оценка по объёму
|
||
- Фазы 1–4 (БД+API+планировщик+АТС-curl): ~0,5–1 день.
|
||
- Фазы 5–9 (UI): ~1–1,5 дня.
|
||
- Фаза 10 (прогоны): несколько часов.
|
||
Итого ≈ 2–2,5 рабочих дня до prod-включения.
|