feat(autocall): звонок клиенту, если SMS не ушло (отсчёт от последней попытки) + статусы «не выбрал день/время» вместо «не нажал цифру»
This commit is contained in:
parent
7ab3debbb3
commit
5c98a54895
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -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 в рабочие дни",
|
||||||
"После двух «взял, но не нажал» или исчерпания попыток передаёт заказ менеджеру",
|
"После двух «взял, но не выбрал» или исчерпания попыток передаёт заказ менеджеру",
|
||||||
"Каждая попытка и результат видны в карточке заказа; ручной звонок идёт клиенту и показывает своё состояние",
|
"Каждая попытка и результат видны в карточке заказа; ручной звонок идёт клиенту и показывает своё состояние",
|
||||||
]},
|
]},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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: "Исключение: по этому номеру или городу не звоним",
|
||||||
|
|
|
||||||
|
|
@ -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("нужен менеджер");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: "Неверный номер",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: "⚠️" },
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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: "⚠️" },
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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: "🤖" },
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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: "Не звоним (исключение)" },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue