feat(autocall): звонок клиенту, если SMS не ушло (отсчёт от последней попытки) + статусы «не выбрал день/время» вместо «не нажал цифру»

This commit is contained in:
Hermes 2026-09-16 16:14:34 +02:00
parent 7ab3debbb3
commit 5c98a54895
13 changed files with 94 additions and 52 deletions

File diff suppressed because one or more lines are too long

View File

@ -16,7 +16,7 @@ TITLE_SLIDE = {
"sub": "Робот звонит клиенту, согласует день и половину дня — без участия оператора", "sub": "Робот звонит клиенту, согласует день и половину дня — без участия оператора",
"lines": [ "lines": [
"Что описано: правила звонка, паузы, поведение в нештатных случаях (не дозвонились,", "Что описано: правила звонка, паузы, поведение в нештатных случаях (не дозвонились,",
"сбросили трубку, не нажали цифру), где что настраивается и что видит менеджер.", "сбросили трубку, не выбрали день/время), где что настраивается и что видит менеджер.",
"", "",
"Основано на текущем коде и настройках боевой системы, без обещаний и продажи.", "Основано на текущем коде и настройках боевой системы, без обещаний и продажи.",
], ],
@ -52,7 +52,7 @@ SLIDES = [
"steps": [ "steps": [
("1. Заказ из 1С", "приходит готовым\nпо обмену"), ("1. Заказ из 1С", "приходит готовым\nпо обмену"),
("2. Ссылка и SMS", "ссылка создаётся\nсама, SMS уходит"), ("2. Ссылка и SMS", "ссылка создаётся\nсама, SMS уходит"),
("3. Пауза 90 минут", "настройка админки:\nстолько ждём ответа"), ("3. Пауза 90 минут", "после SMS или\nпосле попытки, если\nSMS не ушло"),
("4. Звонок робота", "если клиент не\nподтвердил по ссылке"), ("4. Звонок робота", "если клиент не\nподтвердил по ссылке"),
("5. Результат в заказ", "дата и половина дня\nв самом заказе"), ("5. Результат в заказ", "дата и половина дня\nв самом заказе"),
], ],
@ -187,7 +187,7 @@ SLIDES = [
], ],
"full": {"title": "Для менеджера это видно в карточке", "accent": ACCENT2, "items": [ "full": {"title": "Для менеджера это видно в карточке", "accent": ACCENT2, "items": [
"Каждая попытка — отдельная строка истории: время, результат, длительность разговора", "Каждая попытка — отдельная строка истории: время, результат, длительность разговора",
"Статус заказа меняется на «нужен менеджер» — с комментарием «не дозвонились / не нажал»", "Статус заказа меняется на «нужен менеджер» — с комментарием «не дозвонились / не выбрал»",
"Отдельно красным предупреждаем, если заявка «Позвонить сейчас» висит больше 15 минут", "Отдельно красным предупреждаем, если заявка «Позвонить сейчас» висит больше 15 минут",
]}, ]},
}, },
@ -198,8 +198,8 @@ SLIDES = [
"table": { "table": {
"rows": [ "rows": [
["Когда положили трубку", "Что запишет робот", "Что увидит менеджер"], ["Когда положили трубку", "Что запишет робот", "Что увидит менеджер"],
["до выбора дня", "ответил, ничего не нажал", "«взял, не нажал (день)» — попытка зачтена"], ["до выбора дня", "ответил, день не выбрал", "«взял, не выбрал день» — попытка зачтена"],
["день выбрал, время не успел", "день выбран, время нет", "«взял, не нажал (время)»"], ["день выбрал, время не успел", "день выбран, время нет", "«взял, не выбрал время»"],
["выбор подтвердил и сбросил", "слот согласован", "дата и половина дня в заказе, статус «согласовано»"], ["выбор подтвердил и сбросил", "слот согласован", "дата и половина дня в заказе, статус «согласовано»"],
["начал менять выбор и сбросил", "остаётся прежний слот", "«правка не доведена» — прежнее согласование в силе"], ["начал менять выбор и сбросил", "остаётся прежний слот", "«правка не доведена» — прежнее согласование в силе"],
], ],
@ -216,10 +216,10 @@ SLIDES = [
"title": "Когда подключается менеджер", "title": "Когда подключается менеджер",
"sub": "Робот доводит дело до конца сам, но в трёх случаях передаёт человеку", "sub": "Робот доводит дело до конца сам, но в трёх случаях передаёт человеку",
"cards": [ "cards": [
{"title": "Не нажал цифру 2 раза", "accent": WARN, "items": [ {"title": "Не выбрал день/время 2 раза", "accent": WARN, "items": [
"Робот писал «взял, но не нажал» дважды", "Робот дважды фиксировал «взял, но не выбрал»",
"Заказ помечается «нужен менеджер», комментарий «не дозвонились / не нажал»", "Заказ помечается «нужен менеджер», комментарий «не дозвонились / не выбрал»",
"Порог «не нажал × N» настраивается в админке автодозвона", "Порог «не выбрал × N» настраивается в админке автодозвона",
]}, ]},
{"title": "Исчерпаны 5 попыток", "accent": WARN, "items": [ {"title": "Исчерпаны 5 попыток", "accent": WARN, "items": [
"Каждый звонок — отдельная попытка", "Каждый звонок — отдельная попытка",
@ -253,11 +253,11 @@ SLIDES = [
"Вне рабочих часов кампании (09:00–20:00) и вне рабочих дней", "Вне рабочих часов кампании (09:00–20:00) и вне рабочих дней",
"Город исключён целиком или для города выключен канал", "Город исключён целиком или для города выключен канал",
"В админке автодозвона выключены звонки по городу — например, Ялта: SMS идут, звонков нет", "В админке автодозвона выключены звонки по городу — например, Ялта: SMS идут, звонков нет",
"Меньше 90 минут прошло после SMS со ссылкой", "Меньше 90 минут прошло после последней попытки отправки SMS: если приглашение ушло — считаем от отправки, если не ушло — от попытки",
]}, ]},
], ],
"full": {"title": "Админка автодозвона: где это переключается", "accent": ACCENT2, "items": [ "full": {"title": "Админка автодозвона: где это переключается", "accent": ACCENT2, "items": [
"Панель «Автодозвон»: тестовый режим, часы и дни звонков, паузы, лимит попыток, порог «не нажал × N»", "Панель «Автодозвон»: тестовый режим, часы и дни звонков, паузы, лимит попыток, порог «не выбрал × N»",
"Панель «Локации»: для каждого города — звонки включены или выключены (SMS не затрагивается)", "Панель «Локации»: для каждого города — звонки включены или выключены (SMS не затрагивается)",
"Панель исключений: номера и префиксы, на которые не звоним, с причиной", "Панель исключений: номера и префиксы, на которые не звоним, с причиной",
]}, ]},
@ -330,12 +330,12 @@ SLIDES = [
"sub": "Одна страница вместо всей презентации", "sub": "Одна страница вместо всей презентации",
"cards": [ "cards": [
{"title": "Что делает система", "accent": ACCENT, "full": True, "dense": True, "items": [ {"title": "Что делает система", "accent": ACCENT, "full": True, "dense": True, "items": [
"Ждёт 90 минут после SMS и звонит, если клиент не подтвердил день сам", "Ждёт 90 минут после SMS (или после последней попытки отправки, если SMS не ушло) и звонит, если клиент не подтвердил день сам",
"Предлагает только рабочие дни по графику: самовывоз — включая сегодня, доставка — со следующего дня", "Предлагает только рабочие дни по графику: самовывоз — включая сегодня, доставка — со следующего дня",
"Спрашивает день и половину дня, подтверждает вслух, даёт один раз переделать выбор", "Спрашивает день и половину дня, подтверждает вслух, даёт один раз переделать выбор",
"Граница половины дня — 13:00: звоним после 13:00, значит говорим про вторую половину", "Граница половины дня — 13:00: звоним после 13:00, значит говорим про вторую половину",
"Повторяет попытки не чаще чем раз в час, максимум 5 раз, только 09:00–20:00 в рабочие дни", "Повторяет попытки не чаще чем раз в час, максимум 5 раз, только 09:00–20:00 в рабочие дни",
"После двух «взял, но не нажал» или исчерпания попыток передаёт заказ менеджеру", "После двух «взял, но не выбрал» или исчерпания попыток передаёт заказ менеджеру",
"Каждая попытка и результат видны в карточке заказа; ручной звонок идёт клиенту и показывает своё состояние", "Каждая попытка и результат видны в карточке заказа; ручной звонок идёт клиенту и показывает своё состояние",
]}, ]},
], ],

View File

@ -85,16 +85,7 @@ def resolve_scenario(created_at, now=None):
return "A" if created.date() == current.date() else "B" return "A" if created.date() == current.date() else "B"
def get_groups_to_call(conn, settings): GROUPS_TO_CALL_SQL = """
"""Группы, которым пора звонить."""
delay_min = settings.get("delay_after_sms_minutes", 90)
retry_min = settings.get("retry_interval_minutes", 120)
max_attempts = settings.get("max_attempts", 5)
no_input_escalate = settings.get("no_input_escalate_after", 2)
pickup_enabled = bool(settings.get("pickup_calls_enabled", False))
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("""
SELECT og.id, og.customer_phone, og.customer_phone_normalized, SELECT og.id, og.customer_phone, og.customer_phone_normalized,
og.notification_status, og.delivery_status, og.sms_sent_at, og.notification_status, og.delivery_status, og.sms_sent_at,
og.autocall_opt_out, og.created_at, og.delivery_type, og.town, og.autocall_opt_out, og.created_at, og.delivery_type, og.town,
@ -104,14 +95,43 @@ def get_groups_to_call(conn, settings):
WHERE l.order_group_id = og.id WHERE l.order_group_id = og.id
AND l.outcome IN ('no_input_day','no_input_half')) as no_input_count, AND l.outcome IN ('no_input_day','no_input_half')) as no_input_count,
(SELECT max(l.created_at) FROM autocall_log l (SELECT max(l.created_at) FROM autocall_log l
WHERE l.order_group_id = og.id) as last_call_at WHERE l.order_group_id = og.id) as last_call_at,
-- От чего считаем паузу перед звонком:
-- * SMS ушло — от времени отправки;
-- * SMS не ушло/не доставлено — от последней попытки отправки
-- (владелец: «если SMS не отправлено — звоним через 90 минут
-- после последней попытки»).
COALESCE(
CASE WHEN t.try_at IS NOT NULL
AND t.try_status NOT IN ('sent', 'delivered')
THEN t.try_at END,
og.sms_sent_at
) as call_after
FROM order_groups og FROM order_groups og
LEFT JOIN LATERAL (
SELECT COALESCE(l.updated_at, l.created_at) AS try_at, l.status AS try_status
FROM sms_campaign_log l
WHERE l.order_group_id = og.id
AND l.campaign_type IN ('first_sms', 'second_sms')
ORDER BY COALESCE(l.updated_at, l.created_at) DESC
LIMIT 1
) t ON true
WHERE og.delivery_status = 'pending_confirmation' WHERE og.delivery_status = 'pending_confirmation'
AND og.autocall_opt_out = false AND og.autocall_opt_out = false
AND og.notification_status NOT IN ('confirmed', 'manual_required', 'opt_out') AND og.notification_status NOT IN ('confirmed', 'manual_required', 'opt_out')
AND og.sms_sent_at IS NOT NULL -- Пауза после SMS (или после неудачной попытки) прошла
-- Пауза после SMS прошла AND COALESCE(
AND og.sms_sent_at < NOW() - make_interval(mins => %s) CASE WHEN t.try_at IS NOT NULL
AND t.try_status NOT IN ('sent', 'delivered')
THEN t.try_at END,
og.sms_sent_at
) IS NOT NULL
AND COALESCE(
CASE WHEN t.try_at IS NOT NULL
AND t.try_status NOT IN ('sent', 'delivered')
THEN t.try_at END,
og.sms_sent_at
) < NOW() - make_interval(mins => %s)
-- Не звонили или прошёл интервал повтора -- Не звонили или прошёл интервал повтора
AND ( AND (
NOT EXISTS (SELECT 1 FROM autocall_log l WHERE l.order_group_id = og.id) NOT EXISTS (SELECT 1 FROM autocall_log l WHERE l.order_group_id = og.id)
@ -142,8 +162,20 @@ def get_groups_to_call(conn, settings):
) )
-- Самовывоз звоним только если разрешено настройкой -- Самовывоз звоним только если разрешено настройкой
AND (COALESCE(og.delivery_type, 'delivery') <> 'pickup' OR %s) AND (COALESCE(og.delivery_type, 'delivery') <> 'pickup' OR %s)
ORDER BY og.sms_sent_at ASC ORDER BY call_after ASC
""", (delay_min, retry_min, max_attempts, pickup_enabled)) """
def get_groups_to_call(conn, settings):
"""Группы, которым пора звонить."""
delay_min = settings.get("delay_after_sms_minutes", 90)
retry_min = settings.get("retry_interval_minutes", 120)
max_attempts = settings.get("max_attempts", 5)
pickup_enabled = bool(settings.get("pickup_calls_enabled", False))
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(GROUPS_TO_CALL_SQL,
(delay_min, retry_min, max_attempts, pickup_enabled))
return [dict(r) for r in cur.fetchall()] return [dict(r) for r in cur.fetchall()]
def is_work_time(settings, now=None): def is_work_time(settings, now=None):

View File

@ -3,7 +3,7 @@
* @description Бейдж автодозвона в строке реестра доставок. * @description Бейдж автодозвона в строке реестра доставок.
* *
* Раньше наружу выходили две безликие плашки: «Автодозвон ✅» и «Автодозвон ❌» — * Раньше наружу выходили две безликие плашки: «Автодозвон ✅» и «Автодозвон ❌» —
* по ним нельзя было понять, не дозвонились, не нажали цифру или заказ ушёл * по ним нельзя было понять, не дозвонились, не выбрал клиент день/время или заказ ушёл
* менеджеру. Теперь у каждого состояния своя подпись, взятая из * менеджеру. Теперь у каждого состояния своя подпись, взятая из
* AUTOCALL_STATUS_META (тот же список, что в фильтрах, — не разъезжается). * AUTOCALL_STATUS_META (тот же список, что в фильтрах, — не разъезжается).
* *
@ -31,7 +31,7 @@ const STYLES = {
const TITLES = { const TITLES = {
confirmed: "Клиент согласовал день и половину дня по звонку робота", confirmed: "Клиент согласовал день и половину дня по звонку робота",
no_answer: "Робот звонил, трубку не взяли", no_answer: "Робот звонил, трубку не взяли",
no_input: "Трубку взяли, но цифру в меню не нажали", no_input: "Трубку взяли, но день/время клиент не выбрал",
manual: "Нужен менеджер: неверный номер или ошибка звонка", manual: "Нужен менеджер: неверный номер или ошибка звонка",
opt_out: "Звонки по этому клиенту запрещены вручную", opt_out: "Звонки по этому клиенту запрещены вручную",
excluded: "Исключение: по этому номеру или городу не звоним", excluded: "Исключение: по этому номеру или городу не звоним",

View File

@ -1,7 +1,7 @@
/** /**
* @file CallStatusBadge.test.jsx * @file CallStatusBadge.test.jsx
* @description Бейдж автодозвона в реестре доставок: по плашке должно быть понятно, * @description Бейдж автодозвона в реестре доставок: по плашке должно быть понятно,
* что с заказом — не звонили, согласовано, не дозвонились, не нажал цифру, * что с заказом — не звонили, согласовано, не дозвонились, не выбрал день/время,
* нужен менеджер, запрет или исключение. Без «Автодозвон ❌» на всё подряд. * нужен менеджер, запрет или исключение. Без «Автодозвон ❌» на всё подряд.
*/ */
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
@ -32,7 +32,7 @@ describe("resolveCallBadge — что видно в строке", () => {
expect(new Set(labels).size).toBe(labels.length); expect(new Set(labels).size).toBe(labels.length);
expect(labels[0]).toContain("согласовано"); expect(labels[0]).toContain("согласовано");
expect(labels[1]).toContain("не дозвонились"); expect(labels[1]).toContain("не дозвонились");
expect(labels[2]).toContain("не нажал цифру"); expect(labels[2]).toContain("не выбрал день/время");
expect(labels[3]).toContain("нужен менеджер"); expect(labels[3]).toContain("нужен менеджер");
}); });

View File

@ -29,8 +29,8 @@ const OUTCOME_LABELS = {
confirmed_kept_previous: "Правка не доведена", confirmed_kept_previous: "Правка не доведена",
confirmed_silent: "Молчание", confirmed_silent: "Молчание",
no_answer: "Не взял", no_answer: "Не взял",
no_input_day: "Не нажал (день)", no_input_day: "Не выбрал день",
no_input_half: "Не нажал (время)", no_input_half: "Не выбрал время",
operator: "Менеджер", operator: "Менеджер",
opt_out: "Отказ", opt_out: "Отказ",
invalid_number: "Неверный номер", invalid_number: "Неверный номер",

View File

@ -27,8 +27,8 @@ const OUTCOME_META = {
confirmed_kept_previous:{ label: "Правка не доведена", tone: "warning", icon: "🔁" }, confirmed_kept_previous:{ label: "Правка не доведена", tone: "warning", icon: "🔁" },
confirmed_silent: { label: "Подтвердил молчанием", tone: "accent", icon: "🤫" }, confirmed_silent: { label: "Подтвердил молчанием", tone: "accent", icon: "🤫" },
no_answer: { label: "Не взял трубку", tone: "neutral", icon: "📵" }, no_answer: { label: "Не взял трубку", tone: "neutral", icon: "📵" },
no_input_day: { label: "Взял, не нажал (день)", tone: "warning", icon: "🤐" }, no_input_day: { label: "Взял, не выбрал день", tone: "warning", icon: "🤐" },
no_input_half: { label: "Взял, не нажал (время)", tone: "warning", icon: "🤐" }, no_input_half: { label: "Взял, не выбрал время", tone: "warning", icon: "🤐" },
invalid_number: { label: "Неверный номер", tone: "danger", icon: "❌" }, invalid_number: { label: "Неверный номер", tone: "danger", icon: "❌" },
error: { label: "Ошибка", tone: "danger", icon: "⚠️" }, error: { label: "Ошибка", tone: "danger", icon: "⚠️" },
}; };
@ -64,8 +64,8 @@ const slotText = (l) => {
return null; return null;
}; };
const SettingField = ({ label, value, onChange }) => ( const SettingField = ({ label, value, onChange, hint }) => (
<label className="block"> <label className="block" title={hint || undefined}>
<span className="mb-1 block text-[10px] font-medium text-[var(--color-text-muted)]">{label}</span> <span className="mb-1 block text-[10px] font-medium text-[var(--color-text-muted)]">{label}</span>
<input <input
type="text" type="text"
@ -308,11 +308,16 @@ export const AutocallChannelTab = () => {
{/* Интервалы и попытки */} {/* Интервалы и попытки */}
<div className="mb-1 text-xs font-semibold text-[var(--color-text)]">📊 Интервалы и попытки</div> <div className="mb-1 text-xs font-semibold text-[var(--color-text)]">📊 Интервалы и попытки</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
<SettingField label="Пауза после SMS (мин)" value={settings.delay_after_sms_minutes} onChange={(v) => updateSetting("delay_after_sms_minutes", parseInt(v) || 90)} /> <SettingField
label="Пауза после SMS / попытки (мин)"
value={settings.delay_after_sms_minutes}
hint="Столько ждём и после отправленного SMS, и после последней неудачной попытки отправки: если приглашение не ушло, робот всё равно позвонит."
onChange={(v) => updateSetting("delay_after_sms_minutes", parseInt(v) || 90)}
/>
<SettingField label="Между попытками (мин)" value={settings.retry_interval_minutes} onChange={(v) => updateSetting("retry_interval_minutes", parseInt(v) || 120)} /> <SettingField label="Между попытками (мин)" value={settings.retry_interval_minutes} onChange={(v) => updateSetting("retry_interval_minutes", parseInt(v) || 120)} />
<SettingField label="Попыток в день" value={settings.attempts_per_day} onChange={(v) => updateSetting("attempts_per_day", parseInt(v) || 3)} /> <SettingField label="Попыток в день" value={settings.attempts_per_day} onChange={(v) => updateSetting("attempts_per_day", parseInt(v) || 3)} />
<SettingField label="Всего попыток" value={settings.max_attempts} onChange={(v) => updateSetting("max_attempts", parseInt(v) || 5)} /> <SettingField label="Всего попыток" value={settings.max_attempts} onChange={(v) => updateSetting("max_attempts", parseInt(v) || 5)} />
<SettingField label="«Не нажал» ×N → менеджер" value={settings.no_input_escalate_after} onChange={(v) => updateSetting("no_input_escalate_after", parseInt(v) || 2)} /> <SettingField label="«Не выбрал» ×N → менеджер" value={settings.no_input_escalate_after} onChange={(v) => updateSetting("no_input_escalate_after", parseInt(v) || 2)} />
<SettingField label="Попытки след. дня (HH:MM,HH:MM)" value={settings.next_day_attempt_times || ""} onChange={(v) => updateSetting("next_day_attempt_times", v)} /> <SettingField label="Попытки след. дня (HH:MM,HH:MM)" value={settings.next_day_attempt_times || ""} onChange={(v) => updateSetting("next_day_attempt_times", v)} />
</div> </div>

View File

@ -18,8 +18,8 @@ const OUTCOME_META = {
confirmed_kept_previous: { label: "Правка не доведена", tone: "warning", icon: "🔁" }, confirmed_kept_previous: { label: "Правка не доведена", tone: "warning", icon: "🔁" },
confirmed_silent: { label: "Подтвердил молчанием", tone: "accent", icon: "🤫" }, confirmed_silent: { label: "Подтвердил молчанием", tone: "accent", icon: "🤫" },
no_answer: { label: "Не взял трубку", tone: "neutral", icon: "📵" }, no_answer: { label: "Не взял трубку", tone: "neutral", icon: "📵" },
no_input_day: { label: "Взял, не нажал (день)", tone: "warning", icon: "🤐" }, no_input_day: { label: "Взял, не выбрал день", tone: "warning", icon: "🤐" },
no_input_half: { label: "Взял, не нажал (время)", tone: "warning", icon: "🤐" }, no_input_half: { label: "Взял, не выбрал время", tone: "warning", icon: "🤐" },
invalid_number: { label: "Неверный номер", tone: "danger", icon: "❌" }, invalid_number: { label: "Неверный номер", tone: "danger", icon: "❌" },
error: { label: "Ошибка", tone: "danger", icon: "⚠️" }, error: { label: "Ошибка", tone: "danger", icon: "⚠️" },
}; };

View File

@ -124,6 +124,13 @@ export const SmsProblemNoticeView = ({ summary, onClose, className = "" }) => {
<div className={`mt-2 text-xs ${summary.kind === "resolved" ? "text-[var(--color-text-muted)]" : "text-[var(--color-text)]"}`}> <div className={`mt-2 text-xs ${summary.kind === "resolved" ? "text-[var(--color-text-muted)]" : "text-[var(--color-text)]"}`}>
{hint} {hint}
</div> </div>
{summary.kind !== "resolved" && (
<div className="mt-1.5 text-xs text-[var(--color-text-muted)]">
Автодозвон из-за этого не останавливается: робот позвонит клиенту через паузу после последней
попытки отправки — столько же, сколько настроено в «Пауза после SMS (мин)».
</div>
)}
</div> </div>
); );
}; };

View File

@ -14,8 +14,8 @@ const OUTCOME_META = {
confirmed_kept_previous:{ label: "Правка не доведена", tone: "warning", icon: "🔁" }, confirmed_kept_previous:{ label: "Правка не доведена", tone: "warning", icon: "🔁" },
confirmed_silent: { label: "Подтвердил молчанием", tone: "accent", icon: "🤫" }, confirmed_silent: { label: "Подтвердил молчанием", tone: "accent", icon: "🤫" },
no_answer: { label: "Не взял трубку", tone: "neutral", icon: "📵" }, no_answer: { label: "Не взял трубку", tone: "neutral", icon: "📵" },
no_input_day: { label: "Взял, не нажал (день)", tone: "warning", icon: "🤐" }, no_input_day: { label: "Взял, не выбрал день", tone: "warning", icon: "🤐" },
no_input_half: { label: "Взял, не нажал (время)", tone: "warning", icon: "🤐" }, no_input_half: { label: "Взял, не выбрал время", tone: "warning", icon: "🤐" },
invalid_number: { label: "Неверный номер", tone: "danger", icon: "❌" }, invalid_number: { label: "Неверный номер", tone: "danger", icon: "❌" },
error: { label: "Ошибка", tone: "danger", icon: "⚠️" }, error: { label: "Ошибка", tone: "danger", icon: "⚠️" },
}; };

View File

@ -109,10 +109,8 @@ const AUTOCALL_OUTCOME_LABELS = {
confirmed_silent: { label: "Автодозвон: подтвердил молчанием", tone: "ok", icon: "🤖" }, confirmed_silent: { label: "Автодозвон: подтвердил молчанием", tone: "ok", icon: "🤖" },
confirmed_kept_previous:{ label: "Автодозвон: клиент правил выбор", tone: "warning", icon: "🤖" }, confirmed_kept_previous:{ label: "Автодозвон: клиент правил выбор", tone: "warning", icon: "🤖" },
no_answer: { label: "Автодозвон: не взял трубку", tone: "danger", icon: "🤖" }, no_answer: { label: "Автодозвон: не взял трубку", tone: "danger", icon: "🤖" },
no_input_day: { label: "Автодозвон: не нажал (день)", tone: "warning", icon: "🤖" }, no_input_day: { label: "Автодозвон: не выбрал день", tone: "warning", icon: "🤖" },
no_input_half: { label: "Автодозвон: не нажал (время)", tone: "warning", icon: "🤖" }, no_input_half: { label: "Автодозвон: не выбрал время", tone: "warning", icon: "🤖" },
operator: { label: "Автодозвон: клиент попросил менеджера", tone: "warning", icon: "🤖" },
opt_out: { label: "Автодозвон: клиент отказался от звонков", tone: "danger", icon: "🤖" },
invalid_number: { label: "Автодозвон: неверный номер", tone: "danger", icon: "🤖" }, invalid_number: { label: "Автодозвон: неверный номер", tone: "danger", icon: "🤖" },
error: { label: "Автодозвон: ошибка звонка", tone: "danger", icon: "🤖" }, error: { label: "Автодозвон: ошибка звонка", tone: "danger", icon: "🤖" },
}; };

View File

@ -345,7 +345,7 @@ export const AUTOCALL_STATUS_META = {
not_called: { label: "Не звонили", short: "звонка не было", tone: "neutral", icon: "🤖" }, not_called: { label: "Не звонили", short: "звонка не было", tone: "neutral", icon: "🤖" },
confirmed: { label: "Согласовано", short: "согласовано", tone: "accent", icon: "✅" }, confirmed: { label: "Согласовано", short: "согласовано", tone: "accent", icon: "✅" },
no_answer: { label: "Не дозвонились", short: "не дозвонились", tone: "danger", icon: "📵" }, no_answer: { label: "Не дозвонились", short: "не дозвонились", tone: "danger", icon: "📵" },
no_input: { label: "Не нажал в меню", short: "не нажал цифру", tone: "warning", icon: "🤐" }, no_input: { label: "Не выбрал день/время", short: "не выбрал день/время", tone: "warning", icon: "🤐" },
manual: { label: "Нужен менеджер", short: "нужен менеджер", tone: "warning", icon: "🆘" }, manual: { label: "Нужен менеджер", short: "нужен менеджер", tone: "warning", icon: "🆘" },
opt_out: { label: "Не звонить", short: "не звонить", tone: "danger", icon: "🚫" }, opt_out: { label: "Не звонить", short: "не звонить", tone: "danger", icon: "🚫" },
excluded: { label: "Не звоним (исключение)", short: "исключение", tone: "neutral", icon: "🚫" }, excluded: { label: "Не звоним (исключение)", short: "исключение", tone: "neutral", icon: "🚫" },
@ -356,7 +356,7 @@ export const AUTOCALL_STATUS_OPTIONS = [
{ value: "not_called", label: "Не звонили" }, { value: "not_called", label: "Не звонили" },
{ value: "confirmed", label: "Согласовано" }, { value: "confirmed", label: "Согласовано" },
{ value: "no_answer", label: "Не дозвонились" }, { value: "no_answer", label: "Не дозвонились" },
{ value: "no_input", label: "Не нажал в меню" }, { value: "no_input", label: "Не выбрал день/время" },
{ value: "manual", label: "Нужен менеджер" }, { value: "manual", label: "Нужен менеджер" },
{ value: "opt_out", label: "Не звонить (запрет вручную)" }, { value: "opt_out", label: "Не звонить (запрет вручную)" },
{ value: "excluded", label: "Не звоним (исключение)" }, { value: "excluded", label: "Не звоним (исключение)" },