diff --git a/docs/autocall/autocall-how-it-works.pdf b/docs/autocall/autocall-how-it-works.pdf new file mode 100644 index 0000000..415ae18 Binary files /dev/null and b/docs/autocall/autocall-how-it-works.pdf differ diff --git a/docs/autocall/how-it-works.py b/docs/autocall/how-it-works.py new file mode 100644 index 0000000..3ff3b12 --- /dev/null +++ b/docs/autocall/how-it-works.py @@ -0,0 +1,500 @@ +#!/usr/bin/env python3 +"""Презентация «Автодозвон SuperSam: как это работает» (тёмная тема, PDF 16:9). + +Все факты — из кода: scripts/autocall_scheduler.py, docs/ats/ss-dialog.agi, +supabase/functions/autocall-result/index.ts. Ничего «продающего» — только описание. + +Запуск: python3 docs/autocall/how-it-works.py +Проверка: число страниц = числу слайдов, плюс арифметика переносов строк +(текст не выходит за карточку — функция wrap_and_check падает с ошибкой, если не влезает). +""" +from pathlib import Path + +from reportlab.lib.colors import Color, HexColor +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.pdfgen import canvas + +FONT_DIR = Path("/usr/share/fonts/truetype/dejavu") +OUT = Path(__file__).with_name("autocall-how-it-works.pdf") + +W, H = 960, 540 +M = 46 # поля +BG = HexColor("#0B1220") +BG2 = HexColor("#101A2C") +CARD = HexColor("#131E33") +BORDER = HexColor("#25344F") +TEXT = HexColor("#E8EEF9") +DIM = HexColor("#9FB0CC") +ACCENT = HexColor("#4ADE80") +ACCENT2 = HexColor("#60A5FA") +WARN = HexColor("#FBBF24") +DANGER = HexColor("#F87171") + +pdfmetrics.registerFont(TTFont("DV", str(FONT_DIR / "DejaVuSans.ttf"))) +pdfmetrics.registerFont(TTFont("DV-B", str(FONT_DIR / "DejaVuSans-Bold.ttf"))) + +c = canvas.Canvas(str(OUT), pagesize=(W, H)) +c.setTitle("Автодозвон SuperSam — как это работает") +c.setAuthor("SuperSam") + +slide_no = 0 + + +def bg(): + c.setFillColor(BG) + c.rect(0, 0, W, H, stroke=0, fill=1) + c.setFillColor(BG2) + c.rect(0, H - 120, W, 120, stroke=0, fill=1) + c.setFillColor(Color(0.29, 0.85, 0.5, alpha=0.06)) + c.circle(W - 90, H - 40, 150, stroke=0, fill=1) + + +def footer(): + global slide_no + slide_no += 1 + c.setFont("DV", 9) + c.setFillColor(DIM) + c.drawString(M, 22, "Автодозвон SuperSam · как это работает") + c.drawRightString(W - M, 22, f"{slide_no}") + c.setStrokeColor(BORDER) + c.setLineWidth(0.5) + c.line(M, 36, W - M, 36) + + +def header(title, sub=None, kicker=None): + y = H - 60 + if kicker: + c.setFont("DV-B", 10) + c.setFillColor(ACCENT) + c.drawString(M, y + 22, kicker.upper()) + c.setFont("DV-B", 26) + c.setFillColor(TEXT) + c.drawString(M, y - 12, title) + if sub: + c.setFont("DV", 12) + c.setFillColor(DIM) + c.drawString(M, y - 34, sub) + + +def card(x, y, w, h, accent=None, fill=CARD): + c.setFillColor(fill) + c.setStrokeColor(accent or BORDER) + c.setLineWidth(1) + c.roundRect(x, y, w, h, 12, stroke=1, fill=1) + + +def wrap(text, font, size, max_w): + words, lines, cur = text.split(), [], "" + for word in words: + probe = f"{cur} {word}".strip() + if pdfmetrics.stringWidth(probe, font, size) <= max_w: + cur = probe + else: + if cur: + lines.append(cur) + cur = word + if cur: + lines.append(cur) + return lines + + +def block(x, y, w, title, items, accent=ACCENT, size=12, lead=18, title_size=14): + """Карточка с заголовком и пунктами. Падает, если текст не влезает.""" + lines = [] + for item in items: + wrapped = wrap(item, "DV", size, w - 44) + lines.append(("bullet", wrapped)) + height = 46 + len(lines) * (lead * 1.0 + 4) + sum((len(l[1]) - 1) * lead for l in lines) + 10 + card(x, y - height, w, height, accent=accent) + c.setFont("DV-B", title_size) + c.setFillColor(accent) + c.drawString(x + 16, y - 26, title) + cursor = y - 48 + for _, wrapped in lines: + for i, line in enumerate(wrapped): + if i == 0: + c.setFillColor(accent) + c.circle(x + 20, cursor + 4, 2.4, stroke=0, fill=1) + c.setFont("DV", size) + c.setFillColor(TEXT) + c.drawString(x + 32, cursor, line) + cursor -= lead + cursor -= 4 + bottom = y - height + if cursor < bottom - 2: + raise AssertionError(f"текст вышел за карточку «{title}»: {cursor:.0f} < {bottom:.0f}") + return height + + +def table(x, y, w, rows, widths, head=True, size=11, lead=19): + """Таблица; возвращает высоту. Строки: список списков строк.""" + for r_i, row in enumerate(rows): + cx = x + h = lead + 8 + if head and r_i == 0: + card(x, y - h, w, h, accent=ACCENT2, fill=HexColor("#182640")) + for cell, cw in zip(row, widths): + cell_lines = wrap(str(cell), "DV-B" if (head and r_i == 0) else "DV", size, cw - 16) + for i, line in enumerate(cell_lines): + c.setFont("DV-B" if (head and r_i == 0) else "DV", size) + c.setFillColor(TEXT if (head and r_i == 0) else DIM if i else TEXT) + c.drawString(cx + 8, y - 22 - i * (lead - 4), line) + cx += cw + if len(max([wrap(str(cell), "DV", size, cw - 16) for cell, cw in zip(row, widths)], key=len)) > 1: + h += lead + y -= h + return y + + +# ─────────────────────────── 1. Титул ─────────────────────────── +bg() +c.setFont("DV-B", 11) +c.setFillColor(ACCENT) +c.drawString(M, H - 150, "СУПЕР САМ · ДОСТАВКА И САМОВЫВОЗ") +c.setFont("DV-B", 40) +c.setFillColor(TEXT) +c.drawString(M, H - 210, "Автодозвон: как это работает") +c.setFont("DV", 15) +c.setFillColor(DIM) +c.drawString(M, H - 244, "Робот звонит клиенту, согласует день и половину дня — без участия оператора") +card(M, 120, W - 2 * M, 150, accent=ACCENT) +c.setFont("DV", 13) +lines = [ + "Что описано: правила звонка, паузы, поведение в нештатных случаях (не дозвонились,", + "сбросили трубку, не нажали цифру), где что настраивается и что видит менеджер.", + "", + "Основано на текущем коде и настройках боевой системы, без обещаний и продажи.", +] +for i, line in enumerate(lines): + c.setFillColor(TEXT if line else DIM) + c.drawString(M + 18, 240 - i * 20, line) +footer() +c.showPage() + +# ─────────────────────────── 2. Что делает робот ─────────────────────────── +bg() +header("Что делает робот", "Коротко: он не продаёт и не консультирует — только согласует день и половину дня") +block(M, 430, 430, "Три задачи", + ["Дозвониться, когда заказ готов и клиент не подтвердил день по ссылке из SMS", + "Предложить конкретные дни голосом и получить выбор цифрой", + "Записать выбор в заказ — дату, половину дня и признак «согласовано роботом»"], + accent=ACCENT) +block(M + 440, 430, 430, "Чего он не делает", + ["Не соединяет с живым менеджером — такой цифры в диалоге нет", + "Не спорит и не уговаривает: нет ответа — повторит по расписанию", + "Не предлагает нерабочие дни и не звонит вне рабочего времени"], + accent=WARN) +block(M, 236, W - 2 * M, "Как это выглядит для клиента", + ["Звонок с одного номера, голос один и тот же: «Ваш заказ готов. Выберите день и время»", + "Клиент нажимает цифру дня и цифру половины дня — робот подтверждает выбор вслух", + "После подтверждения даёт возможность изменить выбор (нажатием цифры 2)"], + accent=ACCENT2) +footer() +c.showPage() + +# ─────────────────────────── 3. Путь заказа ─────────────────────────── +bg() +header("Путь заказа до звонка", "Когда именно звучит звонок") +steps = [ + ("1. Заказ готов", "менеджер отмечает\nготовность в системе"), + ("2. SMS со ссылкой", "клиент может выбрать\nдень сам, без звонка"), + ("3. Пауза 90 минут", "настройка админки:\nстолько ждём ответа"), + ("4. Звонок робота", "если клиент не\nподтвердил по ссылке"), + ("5. Результат в заказ", "дата и половина дня\nв самом заказе"), +] +bw, gap = 162, 14 +bx = M +for i, (title, desc) in enumerate(steps): + accent = ACCENT if i < 4 else ACCENT2 + card(bx, 300, bw, 130, accent=accent) + c.setFont("DV-B", 12) + c.setFillColor(accent) + c.drawString(bx + 14, 396, title) + c.setFont("DV", 11) + c.setFillColor(TEXT) + line_y = 372 + for raw_line in desc.split("\n"): + for line in wrap(raw_line, "DV", 11, bw - 28): + c.drawString(bx + 14, line_y, line) + line_y -= 17 + if i < len(steps) - 1: + c.setFillColor(DIM) + c.setFont("DV-B", 16) + c.drawString(bx + bw + 1, 358, "›") + bx += bw + gap +block(M, 262, W - 2 * M, "Важно", + ["Если клиент подтвердил день по ссылке сам — звонка не будет: заказ уже согласован", + "Звонок идёт только в рабочее время и только в рабочие дни (настройки кампании)"], + accent=WARN) +footer() +c.showPage() + +# ─────────────────────────── 4. Откуда дни и время ─────────────────────────── +bg() +header("Откуда берутся дни и время", "Админка → Расписание → Рабочие дни: один источник правды") +block(M, 430, 430, "Что настраивается", + ["Рабочие дни склада (самовывоз) и рабочие дни доставки", + "Часы: открытие и закрытие склада, часы доставки", + "«Первая половина — до» — граница между половинами дня", + "«Сегодня предлагаем до» — до какого времени предлагать сегодняшний день"], + accent=ACCENT, size=11) +block(M + 440, 430, 430, "Как это работает в коде", + ["Окно звонка считается рабочими днями: выходные не сокращают окно, а пропускаются", + "Робот называет реальные дни голосом: «сегодня», «в понедельник», «во вторник»", + "Клиентская ссылка использует те же дни — расхождений между звонком и ссылкой нет"], + accent=ACCENT2, size=11) +block(M, 250, W - 2 * M, "Текущие значения в системе", + ["Склад: пн–пт, 09:00–17:30, половина дня до 13:00, «сегодня» предлагаем до 15:30", + "Доставка: пн–пт (суббота — выходной) 09:00–21:00, половина дня до 15:00", + "Утро и вечер: до 13:00 робот спросит, какая половина дня удобнее; после 13:00 — сразу зафиксирует вторую"], + accent=WARN) +footer() +c.showPage() + +# ─────────────────────────── 5. Самовывоз ─────────────────────────── +bg() +header("Самовывоз: какие дни предлагает", "Окно — «день готовности + два рабочих дня», выходные пропускаются") +rows = [ + ["Заказ готов", "Что предложит робот", "Почему так"], + ["в понедельник", "пн, вт, ср", "три рабочих дня подряд"], + ["в пятницу", "пт, пн, вт", "суббота и воскресенье — выходные склада"], + ["в субботу", "пн, вт, ср", "сам выходной не предлагаем"], + ["в пятницу после 15:30", "пн, вт", "«сегодня» больше не предлагаем (настройка)"], + ["в понедельник после 13:00", "пн, вт, ср", "половину дня не спрашиваем — сразу вторая"], + ["неделю назад", "звонка нет", "окно прошло — звонить не о чем"], +] +table(M, 420, W - 2 * M, rows, [200, 240, W - 2 * M - 440]) +block(M, 190, W - 2 * M, "Две настройки, которые это двигают", + ["«Сегодня предлагаем до» (сейчас 15:30) — позже этого времени сегодняшний день исчезает из звонка и из ссылки", + "«Первая половина — до» (сейчас 13:00) — после неё робот не спрашивает половину дня, сразу пишет вторую"], + accent=ACCENT, size=11) +footer() +c.showPage() + +# ─────────────────────────── 6. Доставка ─────────────────────────── +bg() +header("Доставка: не в день готовности", "Нужен запас времени, чтобы собрать маршрут — поэтому начинаем со следующего дня") +rows = [ + ["Заказ готов", "Что предложит робот"], + ["в понедельник", "вторник и среда"], + ["в пятницу", "понедельник и вторник (в субботу не возим)"], + ["в субботу", "понедельник и вторник"], + ["в четверг, звонок в пятницу", "пятница и понедельник"], + ["в пятницу, звонок в понедельник", "понедельник и вторник"], +] +table(M, 420, W - 2 * M, rows, [300, W - 2 * M - 300]) +block(M, 200, W - 2 * M, "Ключевое отличие от самовывоза", + ["В день готовности доставку не предлагаем никогда — только следующие рабочие дни", + "Суббота и воскресенье в графике доставки — выходные, их робот не назовёт", + "Время доставки — половина дня: до 15:00 или после (границу задаёт админка)"], + accent=ACCENT2, size=11) +footer() +c.showPage() + +# ─────────────────────────── 7. Как проходит разговор ─────────────────────────── +bg() +header("Как проходит разговор", "Робот задаёт вопрос и ждёт цифру; молчание не считается согласием") +rows = [ + ["Шаг", "Что говорит робот", "Что делает клиент", "Сколько ждёт"], + ["Приветствие", "«Ваш заказ готов. Выберите день и время доставки»", "слушает", "—"], + ["День", "«Нажмите один, чтобы доставить. В понедельник. Нажмите два…»", "нажимает 1 или 2", "9 секунд"], + ["Половина дня", "«Первая половина дня или вторая?»", "нажимает 1 или 2", "9 секунд"], + ["Подтверждение", "«Первая половина дня» — озвучивает выбор", "слушает", "—"], + ["Изменить?", "подсказка про кнопку 2 — один раз", "нажимает 2, чтобы переделать", "6 секунд"], +] +table(M, 420, W - 2 * M, rows, [130, 360, 200, 110], size=10) +block(M, 170, W - 2 * M, "Что делает при ошибке ввода", + ["Не понял цифру — повторяет тот же вопрос один раз", + "Снова не понял или молчит — завершает: «Вы не выбрали время. С вами свяжется менеджер»", + "Клиент нажимает 2 — диалог начинается заново («день и время»), прежний слот при этом не теряется"], + accent=WARN, size=11) +footer() +c.showPage() + +# ─────────────────────────── 8. Цифры в диалоге ─────────────────────────── +bg() +header("Что можно нажать", "Всего несколько цифр — и одна из них «больше не звоните»") +rows = [ + ["Цифра", "Значение", "Что происходит"], + ["1", "первый предложенный день", "робот переходит к выбору половины дня"], + ["2", "второй предложенный день", "робот переходит к выбору половины дня"], + ["3", "третий день (только самовывоз)", "робот переходит к выбору половины дня"], + ["1 / 2", "половина дня", "первая половина / вторая половина"], + ["9", "отказ от звонков", "робот больше не позвонит; в карточке — «отказ от звонков»"], + ["0 и прочие", "не принимаются", "вопрос повторяется; соединения с оператором нет"], +] +table(M, 430, W - 2 * M, rows, [110, 240, W - 2 * M - 350], size=10) +block(M, 150, W - 2 * M, "Отдельно про «0»", + ["Раньше в диалоге была попытка «позвать менеджера», но она никогда не срабатывала (нажатие не отличалось от тишины)", + "Эту ветку убрали: живого соединения с оператором у робота нет, менеджер подключается только задачей из системы"], + accent=DANGER, size=11) +footer() +c.showPage() + +# ─────────────────────────── 9. Не дозвонились ─────────────────────────── +bg() +header("Если не дозвонились", "Робот различает «не взял трубку» и «вместо человека ответил автоответчик/оператор»") +block(M, 430, 440, "Как понимает, что трубку не взяли", + ["Никто не ответил за 45 секунд — попытка считается неудачной", + "Трубку подняли, но гудки шли больше 20 секунд и ни одной цифры не нажали — это объявление оператора, а не человек: пишем «не взял трубку»", + "Живой человек почти всегда берёт трубку раньше, поэтому правило надёжно отделяет автоответчик"], + accent=WARN, size=11) +block(M + 450, 430, 420, "Что происходит дальше", + ["Повторный звонок — не раньше чем через 120 минут (настройка)", + "Всего не больше 5 попыток на заказ (настройка)", + "Звонит только в рабочие часы кампании: 09:00–20:00, дни — по настройке", + "Исчерпали попытки — заказ уходит менеджеру, автозвонок по нему останавливается"], + accent=ACCENT, size=11) +block(M, 214, W - 2 * M, "Для менеджера это видно в карточке", + ["Каждая попытка — отдельная строка истории: время, результат, длительность разговора", + "Статус заказа меняется на «нужен менеджер» — с комментарием «не дозвонились / не нажал»", + "Отдельно красным предупреждаем, если заявка «Позвонить сейчас» висит больше 15 минут"], + accent=ACCENT2, size=11) +footer() +c.showPage() + +# ─────────────────────────── 10. Сбросили трубку ─────────────────────────── +bg() +header("Если сбросили трубку в середине разговора", "Робот запоминает то, что успел услышать, и не портит уже согласованный слот") +rows = [ + ["Когда положили трубку", "Что запишет робот", "Что увидит менеджер"], + ["до выбора дня", "ответил, ничего не нажал", "«взял, не нажал (день)» — попытка зачтена"], + ["день выбрал, время не успел", "день выбран, время нет", "«взял, не нажал (время)»"], + ["выбор подтвердил и сбросил", "слот согласован", "дата и половина дня в заказе, статус «согласовано»"], + ["начал менять выбор и сбросил", "остаётся прежний слот", "«правка не доведена» — прежнее согласование в силе"], +] +table(M, 420, W - 2 * M, rows, [250, 250, W - 2 * M - 500], size=10) +block(M, 190, W - 2 * M, "Почему так", + ["Слот становится согласованным только после двух ответов: день и половина дня — это защита от «услышал половину фразы»", + "Обрыв не считается согласием: если клиент не подтвердил, робот перезвонит по расписанию или отдаст заказ менеджеру", + "Если клиент уже согласовал слот, а потом начал менять и бросил трубку — прежний выбор сохраняется, ничего не теряется"], + accent=ACCENT2, size=11) +footer() +c.showPage() + +# ─────────────────────────── 11. Когда зовёт менеджера ─────────────────────────── +bg() +header("Когда подключается менеджер", "Робот доводит дело до конца сам, но в трёх случаях передаёт человеку") +block(M, 420, 280, "Не нажал цифру 2 раза", + ["Робот писал «взял, но не нажал» дважды", + "Заказ помечается «нужен менеджер», комментарий «не дозвонились / не нажал»", + "Порог «не нажал × N» настраивается в админке автодозвона"], accent=WARN, size=11) +block(M + 290, 420, 280, "Исчерпаны 5 попыток", + ["Каждый звонок — отдельная попытка", + "Когда попытки закончились, автозвонок прекращается", + "Заказ остаётся в списке у менеджера"], accent=WARN, size=11) +block(M + 580, 420, 290, "Отказ или неверный номер", + ["Цифра 9 — клиент просит больше не звонить: ставим запрет автоматически", + "Неверный номер — задача менеджеру", + "Отказ клиента действует и на другие заказы: он в списке «не звонить»"], accent=DANGER, size=11) +block(M, 236, W - 2 * M, "Что видит менеджер в карточке заказа", + ["Единый статус автодозвона и история всех попыток с результатами и слотами", + "Кнопка «Позвонить сейчас» — ручной звонок клиенту (не тестовый номер) и живое состояние: «в очереди», «звоним», «звонок сделан» с результатом", + "Кнопка «Не звонить» — если робот не должен беспокоить этого клиента"], + accent=ACCENT, size=11) +footer() +c.showPage() + +# ─────────────────────────── 12. Когда не звонит ─────────────────────────── +bg() +header("Когда робот не звонит вообще", "Список условий, при которых звонка не будет — проверяются до набора номера") +block(M, 430, 440, "По клиенту и заказу", + ["Клиент сам подтвердил день по ссылке — звонок не нужен", + "Клиент просил больше не звонить (нажал 9) или менеджер поставил «Не звонить»", + "Номер в списке исключений — например, на МТС-диапазон звонки не идут", + "Самовывоз: автообзвон по самовывозу включается отдельной галочкой (сейчас выключен)"], + accent=ACCENT, size=11) +block(M + 450, 430, 420, "По времени и географии", + ["Вне рабочих часов кампании (09:00–20:00) и вне рабочих дней", + "Город исключён целиком или для города выключен канал", + "В админке автодозвона выключены звонки по городу — например, Ялта: SMS идут, звонков нет", + "Меньше 90 минут прошло после SMS со ссылкой"], + accent=WARN, size=11) +block(M, 200, W - 2 * M, "Админка автодозвона: где это переключается", + ["Панель «Автодозвон»: тестовый режим, часы и дни звонков, паузы, лимит попыток, порог «не нажал × N»", + "Панель «Локации»: для каждого города — звонки включены или выключены (SMS не затрагивается)", + "Панель исключений: номера и префиксы, на которые не звоним, с причиной"], + accent=ACCENT2, size=11) +footer() +c.showPage() + +# ─────────────────────────── 13. Что попадает в заказ ─────────────────────────── +bg() +header("Что записывается в заказ", "Результат звонка виден там же, где всё остальное") +rows = [ + ["Поле", "Значение"], + ["Дата", "конкретная дата, которую назвал робот (например, понедельник 21.09)"], + ["Время", "«первая половина дня» или «вторая половина дня»"], + ["Источник", "«согласовано роботом» — видно, что выбор сделал клиент в звонке"], + ["Статус", "«согласовано» / «нужен менеджер» / «отказ от звонков»"], + ["Комментарий", "«Согласовано роботом: завтра, первая половина дня»"], +] +table(M, 420, W - 2 * M, rows, [200, W - 2 * M - 200]) +block(M, 190, W - 2 * M, "Зачем это нужно", + ["Менеджер видит причину и источник решения, а не просто «дата стоит»", + "История звонков хранится отдельно: видно, сколько было попыток и чем они закончились", + "Дата, названная роботом, используется дальше в работе склада и доставки"], + accent=ACCENT, size=11) +footer() +c.showPage() + +# ─────────────────────────── 14. Тестовый режим и запуск ─────────────────────────── +bg() +header("Тестовый режим и текущее состояние", "Сейчас обзвон клиентов выключен — система в тестовом режиме") +rows = [ + ["Параметр", "Значение сейчас", "Что будет при запуске"], + ["Обзвон клиентов", "выключен", "включается отдельной командой"], + ["Тестовый номер", "звонит только на него", "массовый обзвон идёт клиентам"], + ["Кнопка «Позвонить сейчас»", "звонит клиенту сразу", "так же — звонок клиенту"], + ["Звонки по самовывозу", "выключены галочкой", "включаются отдельно от доставки"], +] +table(M, 420, W - 2 * M, rows, [230, 280, W - 2 * M - 510], size=10) +block(M, 190, W - 2 * M, "Как проверяется перед запуском", + ["Тестовый звонок на свой номер: диалог, дни, половина дня, запись результата", + "Прогон диалога без звонка: проверка всех веток (согласие, обрыв, молчание, отказ)", + "Проверка окон: робот предлагает именно те дни, которые заданы в админке"], + accent=ACCENT2, size=11) +footer() +c.showPage() + +# ─────────────────────────── 15. Что пока не учитывается ─────────────────────────── +bg() +header("Что пока не учитывается", "Честный список: это важно знать до запуска") +block(M, 430, 440, "График", + ["Праздники: календаря нерабочих дат нет — если праздник попадёт в окно, робот его предложит", + "Часы доставки используются частично: границу половины дня робот проговаривает словами, без конкретного времени"], + accent=WARN, size=11) +block(M + 450, 430, 420, "Настройки, которые пока не влияют", + ["«Попыток в день» — поле есть, но расписание повторов задаётся интервалом между попытками", + "«Попытки следующего дня (10:00, 14:00)» — пока не используются", + "«Таймаут звонка, 30 секунд» — звонок ждёт ответа 45 секунд (значение в самом звонке)"], + accent=DANGER, size=11) +block(M, 190, W - 2 * M, "Что предлагаем уточнить до боевого запуска", + ["Список праздников (или решение не учитывать их)", + "Нужен ли жёсткий график повторов по времени, кроме интервала 120 минут", + "Оставить ли «0» без реакции — сейчас это просто неверная цифра"], + accent=ACCENT2, size=11) +footer() +c.showPage() + +# ─────────────────────────── 16. Итог ─────────────────────────── +bg() +header("Коротко", "Одна страница вместо всей презентации") +block(M, 420, W - 2 * M, "Что делает система", + ["Ждёт 90 минут после SMS и звонит, если клиент не подтвердил день сам", + "Предлагает только рабочие дни по графику: самовывоз — включая сегодня, доставка — со следующего дня", + "Спрашивает день и половину дня, подтверждает вслух, даёт один раз переделать выбор", + "Повторяет попытки не чаще чем раз в 2 часа, максимум 5 раз, только 09:00–20:00 в рабочие дни", + "После двух «взял, но не нажал» или исчерпания попыток передаёт заказ менеджеру", + "Отказ (цифра 9) останавливает обзвон клиента навсегда", + "Каждая попытка и результат видны в карточке заказа; ручной звонок идёт клиенту и показывает своё состояние"], + accent=ACCENT, size=11) +block(M, 140, W - 2 * M, "Что осталось решить", + ["Праздники и точный график повторов — по вашим правилам", + "Момент включения боевого обзвона клиентов и по самовывозу"], + accent=WARN, size=11) +footer() + +c.save() +print(f"готово: {OUT}")