diff --git a/docs/autocall/autocall-how-it-works.html b/docs/autocall/autocall-how-it-works.html index 4da8b3b..8526bda 100644 --- a/docs/autocall/autocall-how-it-works.html +++ b/docs/autocall/autocall-how-it-works.html @@ -83,6 +83,6 @@ -
СУПЕР САМ · ДОСТАВКА И САМОВЫВОЗ

Автодозвон: как это работает

Робот звонит клиенту, согласует день и половину дня — без участия оператора

Что описано: правила звонка, паузы, поведение в нештатных случаях (не дозвонились,

сбросили трубку, не нажали цифру), где что настраивается и что видит менеджер.

 

Основано на текущем коде и настройках боевой системы, без обещаний и продажи.

Что делает робот

Коротко: он не продаёт и не консультирует — только согласует день и половину дня

Три задачи

  • Дозвониться, когда заказ готов и клиент не подтвердил день по ссылке из SMS
  • Предложить конкретные дни голосом и получить выбор цифрой
  • Записать выбор в заказ — дату, половину дня и признак «согласовано роботом»

Чего он не делает

  • Не соединяет с живым менеджером — такой цифры в диалоге нет
  • Не спорит и не уговаривает: нет ответа — повторит по расписанию
  • Не предлагает нерабочие дни и не звонит вне рабочего времени

Как это выглядит для клиента

Путь заказа до звонка

Когда именно звучит звонок

1. Заказ готовменеджер отмечаетготовность в системе
›
2. SMS со ссылкойклиент может выбратьдень сам, без звонка
›
3. Пауза 90 минутнастройка админки:столько ждём ответа
›
4. Звонок роботаесли клиент неподтвердил по ссылке
›
5. Результат в заказдата и половина дняв самом заказе

Важно

Откуда берутся дни и время

Админка → Расписание → Рабочие дни: один источник правды

Что настраивается

  • Рабочие дни склада (самовывоз) и рабочие дни доставки
  • Часы: открытие и закрытие склада, часы доставки
  • «Первая половина — до» — граница между половинами дня
  • «Сегодня предлагаем до» — до какого времени предлагать сегодняшний день

Как это работает в коде

  • Окно звонка считается рабочими днями: выходные не сокращают окно, а пропускаются
  • Робот называет реальные дни голосом: «сегодня», «в понедельник», «во вторник»
  • Клиентская ссылка использует те же дни — расхождений между звонком и ссылкой нет

Текущие значения в системе

Самовывоз: какие дни предлагает

Окно — «день готовности + два рабочих дня», выходные пропускаются

Заказ готовЧто предложит роботПочему так
в понедельникпн, вт, сртри рабочих дня подряд
в пятницупт, пн, втсуббота и воскресенье — выходные склада
в субботупн, вт, срсам выходной не предлагаем
в пятницу после 15:30пн, вт«сегодня» больше не предлагаем (настройка)
в понедельник после 13:00пн, вт, срполовину дня не спрашиваем — сразу вторая
неделю назадзвонка нетокно прошло — звонить не о чем

Две настройки, которые это двигают

Доставка: не в день готовности

Нужен запас времени, чтобы собрать маршрут — поэтому начинаем со следующего дня

Заказ готовЧто предложит робот
в понедельниквторник и среда
в пятницупонедельник и вторник (в субботу не возим)
в субботупонедельник и вторник
в четверг, звонок в пятницупятница и понедельник
в пятницу, звонок в понедельникпонедельник и вторник

Ключевое отличие от самовывоза

Как проходит разговор

Робот задаёт вопрос и ждёт цифру; молчание не считается согласием

ШагЧто говорит роботЧто делает клиентСколько ждёт
Приветствие«Ваш заказ готов. Выберите день и время доставки»слушает—
День«Нажмите один, чтобы доставить. В понедельник. Нажмите два…»нажимает 1 или 29 секунд
Половина дня«Первая половина дня или вторая?»нажимает 1 или 29 секунд
Подтверждение«Первая половина дня» — озвучивает выборслушает—
Изменить?подсказка про кнопку 2 — один разнажимает 2, чтобы переделать6 секунд

Что делает при ошибке ввода

Что можно нажать

Всего несколько цифр — и одна из них «больше не звоните»

ЦифраЗначениеЧто происходит
1первый предложенный деньробот переходит к выбору половины дня
2второй предложенный деньробот переходит к выбору половины дня
3третий день (только самовывоз)робот переходит к выбору половины дня
1 / 2половина дняпервая половина / вторая половина
9отказ от звонковробот больше не позвонит; в карточке — «отказ от звонков»
0 и прочиене принимаютсявопрос повторяется; соединения с оператором нет

Отдельно про «0»

Если не дозвонились

Робот различает «не взял трубку» и «вместо человека ответил автоответчик/оператор»

Как понимает, что трубку не взяли

  • Никто не ответил за 45 секунд — попытка считается неудачной
  • Трубку подняли, но гудки шли больше 20 секунд и ни одной цифры не нажали — это объявление оператора, а не человек: пишем «не взял трубку»
  • Живой человек почти всегда берёт трубку раньше, поэтому правило надёжно отделяет автоответчик

Что происходит дальше

  • Повторный звонок — не раньше чем через 120 минут (настройка)
  • Всего не больше 5 попыток на заказ (настройка)
  • Звонит только в рабочие часы кампании: 09:00–20:00, дни — по настройке
  • Исчерпали попытки — заказ уходит менеджеру, автозвонок по нему останавливается

Для менеджера это видно в карточке

Если сбросили трубку в середине разговора

Робот запоминает то, что успел услышать, и не портит уже согласованный слот

Когда положили трубкуЧто запишет роботЧто увидит менеджер
до выбора дняответил, ничего не нажал«взял, не нажал (день)» — попытка зачтена
день выбрал, время не успелдень выбран, время нет«взял, не нажал (время)»
выбор подтвердил и сбросилслот согласовандата и половина дня в заказе, статус «согласовано»
начал менять выбор и сбросилостаётся прежний слот«правка не доведена» — прежнее согласование в силе

Почему так

Когда подключается менеджер

Робот доводит дело до конца сам, но в трёх случаях передаёт человеку

Не нажал цифру 2 раза

  • Робот писал «взял, но не нажал» дважды
  • Заказ помечается «нужен менеджер», комментарий «не дозвонились / не нажал»
  • Порог «не нажал × N» настраивается в админке автодозвона

Исчерпаны 5 попыток

  • Каждый звонок — отдельная попытка
  • Когда попытки закончились, автозвонок прекращается
  • Заказ остаётся в списке у менеджера

Отказ или неверный номер

  • Цифра 9 — клиент просит больше не звонить: ставим запрет автоматически
  • Неверный номер — задача менеджеру
  • Отказ клиента действует и на другие заказы: он в списке «не звонить»

Что видит менеджер в карточке заказа

Когда робот не звонит вообще

Список условий, при которых звонка не будет — проверяются до набора номера

По клиенту и заказу

  • Клиент сам подтвердил день по ссылке — звонок не нужен
  • Клиент просил больше не звонить (нажал 9) или менеджер поставил «Не звонить»
  • Номер в списке исключений — например, на МТС-диапазон звонки не идут
  • Самовывоз: автообзвон по самовывозу включается отдельной галочкой (сейчас выключен)

По времени и географии

  • Вне рабочих часов кампании (09:00–20:00) и вне рабочих дней
  • Город исключён целиком или для города выключен канал
  • В админке автодозвона выключены звонки по городу — например, Ялта: SMS идут, звонков нет
  • Меньше 90 минут прошло после SMS со ссылкой

Админка автодозвона: где это переключается

Что записывается в заказ

Результат звонка виден там же, где всё остальное

ПолеЗначение
Датаконкретная дата, которую назвал робот (например, понедельник 21.09)
Время«первая половина дня» или «вторая половина дня»
Источник«согласовано роботом» — видно, что выбор сделал клиент в звонке
Статус«согласовано» / «нужен менеджер» / «отказ от звонков»
Комментарий«Согласовано роботом: завтра, первая половина дня»

Зачем это нужно

Тестовый режим и текущее состояние

Сейчас обзвон клиентов выключен — система в тестовом режиме

ПараметрЗначение сейчасЧто будет при запуске
Обзвон клиентоввыключенвключается отдельной командой
Тестовый номерзвонит только на негомассовый обзвон идёт клиентам
Кнопка «Позвонить сейчас»звонит клиенту сразутак же — звонок клиенту
Звонки по самовывозувыключены галочкойвключаются отдельно от доставки

Как проверяется перед запуском

Что пока не учитывается

Честный список: это важно знать до запуска

График

  • Праздники: календаря нерабочих дат нет — если праздник попадёт в окно, робот его предложит
  • Часы доставки используются частично: границу половины дня робот проговаривает словами, без конкретного времени

Настройки, которые пока не влияют

  • «Попыток в день» — поле есть, но расписание повторов задаётся интервалом между попытками
  • «Попытки следующего дня (10:00, 14:00)» — пока не используются
  • «Таймаут звонка, 30 секунд» — звонок ждёт ответа 45 секунд (значение в самом звонке)

Что предлагаем уточнить до боевого запуска

Коротко

Одна страница вместо всей презентации

Что делает система

  • Ждёт 90 минут после SMS и звонит, если клиент не подтвердил день сам
  • Предлагает только рабочие дни по графику: самовывоз — включая сегодня, доставка — со следующего дня
  • Спрашивает день и половину дня, подтверждает вслух, даёт один раз переделать выбор
  • Повторяет попытки не чаще чем раз в 2 часа, максимум 5 раз, только 09:00–20:00 в рабочие дни
  • После двух «взял, но не нажал» или исчерпания попыток передаёт заказ менеджеру
  • Отказ (цифра 9) останавливает обзвон клиента навсегда
  • Каждая попытка и результат видны в карточке заказа; ручной звонок идёт клиенту и показывает своё состояние

Что осталось решить

+
СУПЕР САМ · ДОСТАВКА И САМОВЫВОЗ

Автодозвон: как это работает

Робот звонит клиенту, согласует день и половину дня — без участия оператора

Что описано: правила звонка, паузы, поведение в нештатных случаях (не дозвонились,

сбросили трубку, не нажали цифру), где что настраивается и что видит менеджер.

 

Основано на текущем коде и настройках боевой системы, без обещаний и продажи.

Что делает робот

Коротко: он не продаёт и не консультирует — только согласует день и половину дня

Три задачи

  • Дозвониться, когда заказ готов и клиент не подтвердил день по ссылке из SMS
  • Предложить конкретные дни голосом и получить выбор цифрой
  • Записать выбор в заказ — дату, половину дня и признак «согласовано роботом»

Чего он не делает

  • Не соединяет с живым менеджером — такой цифры в диалоге нет
  • Не спорит и не уговаривает: нет ответа — повторит по расписанию
  • Не предлагает нерабочие дни и не звонит вне рабочего времени

Как это выглядит для клиента

Путь заказа до звонка

Когда именно звучит звонок

1. Заказ из 1Сприходит готовымпо обмену
›
2. Ссылка и SMSссылка создаётсясама, SMS уходит
›
3. Пауза 90 минутнастройка админки:столько ждём ответа
›
4. Звонок роботаесли клиент неподтвердил по ссылке
›
5. Результат в заказдата и половина дняв самом заказе

Важно

Откуда берутся дни и время

Админка → Расписание → Рабочие дни: один источник правды

Что настраивается

  • Рабочие дни склада (самовывоз) и рабочие дни доставки
  • Часы: открытие и закрытие склада, часы доставки
  • «Первая половина — до» — граница между половинами дня
  • «Сегодня предлагаем до» — до какого времени предлагать сегодняшний день

Как это работает в коде

  • Окно звонка считается рабочими днями: выходные не сокращают окно, а пропускаются
  • Робот называет реальные дни голосом: «сегодня», «в понедельник», «во вторник»
  • Клиентская ссылка использует те же дни — расхождений между звонком и ссылкой нет

Текущие значения в системе

Самовывоз: какие дни предлагает

Окно — «день готовности + два рабочих дня», выходные пропускаются

Заказ готовЧто предложит роботПочему так
в понедельникпн, вт, сртри рабочих дня подряд
в пятницупт, пн, втсуббота и воскресенье — выходные склада
в субботупн, вт, срсам выходной не предлагаем
в пятницу после 15:30пн, вт«сегодня» больше не предлагаем (настройка)
в понедельник после 13:00пн, вт, срполовину дня не спрашиваем — сразу вторая
неделю назадзвонка нетокно прошло — звонить не о чем

Две настройки, которые это двигают

Доставка: не в день готовности

Нужен запас времени, чтобы собрать маршрут — поэтому начинаем со следующего дня

Заказ готовЧто предложит робот
в понедельниквторник и среда
в пятницупонедельник и вторник (в субботу не возим)
в субботупонедельник и вторник
в четверг, звонок в пятницупятница и понедельник
в пятницу, звонок в понедельникпонедельник и вторник

Ключевое отличие от самовывоза

Как проходит разговор

Робот задаёт вопрос и ждёт цифру; молчание не считается согласием

ШагЧто говорит роботЧто делает клиентСколько ждёт
Приветствие«Ваш заказ готов. Выберите день и время доставки»слушает—
День«Нажмите один, чтобы доставить. В понедельник. Нажмите два…»нажимает 1 или 29 секунд
Половина дня«Первая половина дня или вторая?»нажимает 1 или 29 секунд
Подтверждение«Первая половина дня» — озвучивает выборслушает—
Изменить?подсказка про кнопку 2 — один разнажимает 2, чтобы переделать6 секунд

Что делает при ошибке ввода

Что можно нажать

Всего несколько цифр — и одна из них «больше не звоните»

ЦифраЗначениеЧто происходит
1первый предложенный деньробот переходит к выбору половины дня
2второй предложенный деньробот переходит к выбору половины дня
3третий день (только самовывоз)робот переходит к выбору половины дня
1 / 2половина дняпервая половина / вторая половина
9отказ от звонковробот больше не позвонит; в карточке — «отказ от звонков»
0 и прочиене принимаютсявопрос повторяется; соединения с оператором нет

Отдельно про «0»

Если не дозвонились

Робот различает «не взял трубку» и «вместо человека ответил автоответчик/оператор»

Как понимает, что трубку не взяли

  • Никто не ответил за 45 секунд — попытка считается неудачной
  • Трубку подняли, но гудки шли больше 20 секунд и ни одной цифры не нажали — это объявление оператора, а не человек: пишем «не взял трубку»
  • Живой человек почти всегда берёт трубку раньше, поэтому правило надёжно отделяет автоответчик

Что происходит дальше

  • Повторный звонок — не раньше чем через 120 минут (настройка)
  • Всего не больше 5 попыток на заказ (настройка)
  • Звонит только в рабочие часы кампании: 09:00–20:00, дни — по настройке
  • Исчерпали попытки — заказ уходит менеджеру, автозвонок по нему останавливается

Для менеджера это видно в карточке

Если сбросили трубку в середине разговора

Робот запоминает то, что успел услышать, и не портит уже согласованный слот

Когда положили трубкуЧто запишет роботЧто увидит менеджер
до выбора дняответил, ничего не нажал«взял, не нажал (день)» — попытка зачтена
день выбрал, время не успелдень выбран, время нет«взял, не нажал (время)»
выбор подтвердил и сбросилслот согласовандата и половина дня в заказе, статус «согласовано»
начал менять выбор и сбросилостаётся прежний слот«правка не доведена» — прежнее согласование в силе

Почему так

Когда подключается менеджер

Робот доводит дело до конца сам, но в трёх случаях передаёт человеку

Не нажал цифру 2 раза

  • Робот писал «взял, но не нажал» дважды
  • Заказ помечается «нужен менеджер», комментарий «не дозвонились / не нажал»
  • Порог «не нажал × N» настраивается в админке автодозвона

Исчерпаны 5 попыток

  • Каждый звонок — отдельная попытка
  • Когда попытки закончились, автозвонок прекращается
  • Заказ остаётся в списке у менеджера

Отказ или неверный номер

  • Цифра 9 — клиент просит больше не звонить: ставим запрет автоматически
  • Неверный номер — задача менеджеру
  • Отказ клиента действует и на другие заказы: он в списке «не звонить»

Что видит менеджер в карточке заказа

Когда робот не звонит вообще

Список условий, при которых звонка не будет — проверяются до набора номера

По клиенту и заказу

  • Клиент сам подтвердил день по ссылке — звонок не нужен
  • Клиент просил больше не звонить (нажал 9) или менеджер поставил «Не звонить»
  • Номер в списке исключений — например, на МТС-диапазон звонки не идут
  • Самовывоз: автообзвон по самовывозу включается отдельной галочкой (сейчас выключен)

По времени и географии

  • Вне рабочих часов кампании (09:00–20:00) и вне рабочих дней
  • Город исключён целиком или для города выключен канал
  • В админке автодозвона выключены звонки по городу — например, Ялта: SMS идут, звонков нет
  • Меньше 90 минут прошло после SMS со ссылкой

Админка автодозвона: где это переключается

Что записывается в заказ

Результат звонка виден там же, где всё остальное

ПолеЗначение
Датаконкретная дата, которую назвал робот (например, понедельник 21.09)
Время«первая половина дня» или «вторая половина дня»
Источник«согласовано роботом» — видно, что выбор сделал клиент в звонке
Статус«согласовано» / «нужен менеджер» / «отказ от звонков»
Комментарий«Согласовано роботом: завтра, первая половина дня»

Зачем это нужно

Тестовый режим и текущее состояние

Сейчас обзвон клиентов выключен — система в тестовом режиме

ПараметрЗначение сейчасЧто будет при запуске
Обзвон клиентоввыключенвключается отдельной командой
Тестовый номерзвонит только на негомассовый обзвон идёт клиентам
Кнопка «Позвонить сейчас»звонит клиенту сразутак же — звонок клиенту
Звонки по самовывозувыключены галочкойвключаются отдельно от доставки

Как проверяется перед запуском

Что пока не учитывается

Честный список: это важно знать до запуска

График

  • Праздники: календаря нерабочих дат нет — если праздник попадёт в окно, робот его предложит
  • Часы доставки используются частично: границу половины дня робот проговаривает словами, без конкретного времени

Настройки, которые пока не влияют

  • «Попыток в день» — поле есть, но расписание повторов задаётся интервалом между попытками
  • «Попытки следующего дня (10:00, 14:00)» — пока не используются
  • «Таймаут звонка, 30 секунд» — звонок ждёт ответа 45 секунд (значение в самом звонке)

Что предлагаем уточнить до боевого запуска

Коротко

Одна страница вместо всей презентации

Что делает система

  • Ждёт 90 минут после SMS и звонит, если клиент не подтвердил день сам
  • Предлагает только рабочие дни по графику: самовывоз — включая сегодня, доставка — со следующего дня
  • Спрашивает день и половину дня, подтверждает вслух, даёт один раз переделать выбор
  • Повторяет попытки не чаще чем раз в 2 часа, максимум 5 раз, только 09:00–20:00 в рабочие дни
  • После двух «взял, но не нажал» или исчерпания попыток передаёт заказ менеджеру
  • Отказ (цифра 9) останавливает обзвон клиента навсегда
  • Каждая попытка и результат видны в карточке заказа; ручной звонок идёт клиенту и показывает своё состояние

Что осталось решить

diff --git a/docs/autocall/autocall-how-it-works.pdf b/docs/autocall/autocall-how-it-works.pdf index 415ae18..592d81a 100644 Binary files a/docs/autocall/autocall-how-it-works.pdf and b/docs/autocall/autocall-how-it-works.pdf differ diff --git a/docs/autocall/deck_content.py b/docs/autocall/deck_content.py index 50f9896..aeebd71 100644 --- a/docs/autocall/deck_content.py +++ b/docs/autocall/deck_content.py @@ -50,13 +50,14 @@ SLIDES = [ "title": "Путь заказа до звонка", "sub": "Когда именно звучит звонок", "steps": [ - ("1. Заказ готов", "менеджер отмечает\nготовность в системе"), - ("2. SMS со ссылкой", "клиент может выбрать\nдень сам, без звонка"), + ("1. Заказ из 1С", "приходит готовым\nпо обмену"), + ("2. Ссылка и SMS", "ссылка создаётся\nсама, SMS уходит"), ("3. Пауза 90 минут", "настройка админки:\nстолько ждём ответа"), ("4. Звонок робота", "если клиент не\nподтвердил по ссылке"), ("5. Результат в заказ", "дата и половина дня\nв самом заказе"), ], "full": {"title": "Важно", "accent": WARN, "items": [ + "Готовность заказа приходит из 1С по обмену: заказ приходит уже готовым, вручную в системе её никто не отмечает", "Если клиент подтвердил день по ссылке сам — звонка не будет: заказ уже согласован", "Звонок идёт только в рабочее время и только в рабочие дни (настройки кампании)", ]}, diff --git a/docs/autocall/how-it-works.py b/docs/autocall/how-it-works.py index 3ff3b12..050557e 100644 --- a/docs/autocall/how-it-works.py +++ b/docs/autocall/how-it-works.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 -"""Презентация «Автодозвон SuperSam: как это работает» (тёмная тема, PDF 16:9). +"""PDF-версия презентации «Автодозвон: как это работает» (тёмная тема, 16:9). -Все факты — из кода: scripts/autocall_scheduler.py, docs/ats/ss-dialog.agi, -supabase/functions/autocall-result/index.ts. Ничего «продающего» — только описание. +Содержание — из deck_content.py (то же, что в HTML-версии), здесь только вёрстка. +Текст, который не влезает в карточку, считается ошибкой: скрипт падает, а не рисует +поверх границ. Запуск: python3 docs/autocall/how-it-works.py -Проверка: число страниц = числу слайдов, плюс арифметика переносов строк -(текст не выходит за карточку — функция wrap_and_check падает с ошибкой, если не влезает). """ from pathlib import Path @@ -15,21 +14,26 @@ from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen import canvas +import deck_content as content + FONT_DIR = Path("/usr/share/fonts/truetype/dejavu") OUT = Path(__file__).with_name("autocall-how-it-works.pdf") W, H = 960, 540 -M = 46 # поля +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") +TONES = { + "accent": HexColor("#4ADE80"), + "accent2": HexColor("#60A5FA"), + "warn": HexColor("#FBBF24"), + "danger": HexColor("#F87171"), +} +ACCENT = TONES["accent"] pdfmetrics.registerFont(TTFont("DV", str(FONT_DIR / "DejaVuSans.ttf"))) pdfmetrics.registerFont(TTFont("DV-B", str(FONT_DIR / "DejaVuSans-Bold.ttf"))) @@ -39,6 +43,26 @@ c.setTitle("Автодозвон SuperSam — как это работает") c.setAuthor("SuperSam") slide_no = 0 +CONTENT_W = W - 2 * M + + +def tone(name): + return TONES.get(name, ACCENT) + + +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 bg(): @@ -84,35 +108,23 @@ def card(x, y, w, h, accent=None, fill=CARD): 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_size(items, w, size=12, lead=18, title_size=14): + """Сколько места займёт карточка: список строк и высота.""" + wrapped = [wrap(item, "DV", size, w - 44) for item in items] + lines = sum(len(w_) for w_ in wrapped) + height = 50 + lines * lead + len(items) * 4 + return wrapped, height -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) +def draw_block(x, y, w, h, title, items, accent=ACCENT, size=12, lead=18, title_size=14): + wrapped, _ = block_size(items, w, size, lead, title_size) + card(x, y - h, w, h, 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): + for wrapped_item in wrapped: + for i, line in enumerate(wrapped_item): if i == 0: c.setFillColor(accent) c.circle(x + 20, cursor + 4, 2.4, stroke=0, fill=1) @@ -121,380 +133,151 @@ def block(x, y, w, title, items, accent=ACCENT, size=12, lead=18, title_size=14) 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 + if cursor < (y - h) - 2: + raise AssertionError(f"текст вышел за карточку «{title}»") + return y - h -def table(x, y, w, rows, widths, head=True, size=11, lead=19): - """Таблица; возвращает высоту. Строки: список списков строк.""" +def table_measure(rows, widths, size=11, lead=19): + """Высота таблицы — чтобы можно было отцентровать блок по слайду.""" + h = 0 + for row in rows: + lines = max(len(wrap(str(cell), "DV", size, cw - 16)) for cell, cw in zip(row, widths)) + h += lead + 8 + (lines - 1) * (lead - 4) + return h + + +def cards_row(cards, y, total_w=CONTENT_W, gap=14): + """Ряд карточек одинаковой высоты; возвращает нижнюю границу ряда.""" + n = len(cards) + w = (total_w - gap * (n - 1)) / n + heights = [block_size(card_["items"], w)[1] for card_ in cards] + h = max(heights) + x = M + for card_ in cards: + draw_block(x, y, w, h, card_["title"], card_["items"], accent=tone(card_.get("accent"))) + x += w + gap + return y - h + + +def table(rows, widths, y, size=11, lead=19, head=True): + """Таблица; возвращает нижнюю границу.""" for r_i, row in enumerate(rows): - cx = x - h = lead + 8 + lines_per_cell = [wrap(str(cell), "DV", size, cw - 16) for cell, cw in zip(row, widths)] + h = lead + 8 + (max(len(l) for l in lines_per_cell) - 1) * (lead - 4) 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) + card(M, y - h, CONTENT_W, h, accent=TONES["accent2"], fill=HexColor("#182640")) + cx = M + for (cell, cw), cell_lines in zip(zip(row, widths), lines_per_cell): + if head and r_i == 0: + cx += cw + continue 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) + c.setFont("DV", size) + c.setFillColor(DIM if (i == 0 and cx == M) else TEXT) + c.drawString(cx + 8, y - 21 - 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 + if head and r_i == 0: + cx = M + for cell, cw in zip(row, widths): + c.setFont("DV-B", size) + c.setFillColor(TEXT) + c.drawString(cx + 8, y - 21, str(cell)) + cx += cw 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() +def steps_row(steps, y): + n = len(steps) + gap = 14 + w = (CONTENT_W - gap * (n - 1)) / n + h = 170 + x = M + for i, (title, desc) in enumerate(steps): + accent = ACCENT if i < n - 1 else TONES["accent2"] + card(x, y - h, w, h, accent=accent) + c.setFont("DV-B", 12) + c.setFillColor(accent) + c.drawString(x + 14, y - 34, title) + c.setFont("DV", 11) + c.setFillColor(TEXT) + line_y = y - 58 + for raw_line in desc.split("\n"): + for line in wrap(raw_line, "DV", 11, w - 28): + c.drawString(x + 14, line_y, line) + line_y -= 17 + if i < n - 1: + c.setFillColor(DIM) + c.setFont("DV-B", 16) + c.drawString(x + w + 1, y - 72, "›") + x += w + gap + return y - h -# ─────────────────────────── 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: +def render(slide): + bg() + if slide["kind"] == "title": + c.setFont("DV-B", 11) + c.setFillColor(ACCENT) + c.drawString(M, H - 150, slide["kicker"]) + c.setFont("DV-B", 40) + c.setFillColor(TEXT) + c.drawString(M, H - 210, slide["title"]) + c.setFont("DV", 15) 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() + c.drawString(M, H - 244, slide["sub"]) + card(M, 120, CONTENT_W, 150, accent=ACCENT) + for i, line in enumerate(slide["lines"]): + c.setFont("DV", 13) + c.setFillColor(TEXT if line else DIM) + c.drawString(M + 18, 240 - i * 20, line) + footer() + c.showPage() + return -# ─────────────────────────── 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() + header(slide["title"], slide.get("sub")) + kind = slide["kind"] + full = slide.get("full") -# ─────────────────────────── 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() + # Измеряем содержимое и центруем блок по вертикали: иначе внизу слайда + # остаётся пустая полоса, а на другом слайде текст поджимает футер. + if kind == "steps": + main_h = 170 + elif kind in ("cards2full", "cards3full"): + n = len(slide["cards"]) + w = (CONTENT_W - 14 * (n - 1)) / n + main_h = max(block_size(card_["items"], w)[1] for card_ in slide["cards"]) + elif kind == "tablecard": + main_h = table_measure(slide["table"]["rows"], slide["table"]["widths"]) + else: + raise AssertionError(f"неизвестный тип слайда: {kind}") -# ─────────────────────────── 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() + full_h = block_size(full["items"], CONTENT_W)[1] if full else 0 + gap = 22 if full else 0 + top, bottom_limit = H - 96, 56 + total = main_h + (gap + full_h if full else 0) + y0 = top - max(0.0, (top - bottom_limit - total) / 2) -# ─────────────────────────── 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() + if kind == "steps": + bottom = steps_row(slide["steps"], y0) + elif kind in ("cards2full", "cards3full"): + bottom = cards_row(slide["cards"], y0) + else: + bottom = table(slide["table"]["rows"], slide["table"]["widths"], y0) -# ─────────────────────────── 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() + if full: + y = bottom - gap + if y - full_h < 44: + raise AssertionError(f"нижняя карточка не влезает на слайде «{slide['title']}»") + draw_block(M, y, CONTENT_W, full_h, full["title"], full["items"], accent=tone(full.get("accent"))) + 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() +for slide_ in content.ALL_SLIDES: + render(slide_) c.save() -print(f"готово: {OUT}") +print(f"готово: {OUT} — слайдов {slide_no}") +print(f"вёрстка проверена: все карточки по размеру текста (проверено {slide_no} слайдов)")