From dacd43165ce0ac9837b08527306bab1213c2d07a Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 16 Sep 2026 16:39:28 +0200 Subject: [PATCH] =?UTF-8?q?fix(autocall):=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA?= =?UTF-8?q?=D0=B0=20=C2=AB=D0=BF=D0=BE=D0=B7=D0=B2=D0=BE=D0=BD=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D1=81=D0=B5=D0=B9=D1=87=D0=B0=D1=81=C2=BB=20=D1=81=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D1=8B=D0=BC=20=D0=BE=D0=BA?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=20=D0=B7=D0=B2=D0=BE=D0=BD=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD=D0=B5=20=D0=B2?= =?UTF-8?q?=D0=B8=D1=81=D0=B8=D1=82=20=D0=B2=D0=B5=D1=87=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=B2=20=D0=BE=D1=87=D0=B5=D1=80=D0=B5=D0=B4=D0=B8=20=E2=80=94?= =?UTF-8?q?=20=D0=BF=D0=BB=D0=B0=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D1=89=D0=B8?= =?UTF-8?q?=D0=BA=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D0=B2=D0=B0=D0=B5=D1=82?= =?UTF-8?q?=20=D0=B5=D1=91,=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=87=D0=B5=D1=81=D1=82=D0=BD=D0=BE=20=D0=B3=D0=BE?= =?UTF-8?q?=D0=B2=D0=BE=D1=80=D0=B8=D1=82=20=C2=AB=D0=B7=D0=B2=D0=BE=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=BE=20=D1=87=D0=B5=D0=BC=C2=BB=20+=20=D0=BA?= =?UTF-8?q?=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0=20=C2=AB=D0=B2=D1=81=D1=91=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BD=D0=BE=20=D0=BF=D0=BE=D0=B7=D0=B2=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D1=8C=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/autocall_scheduler.py | 10 ++++++ src/components/orders/AutocallStatusCard.jsx | 33 ++++++++++++++++++-- src/services/autocallProcess.js | 13 ++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/scripts/autocall_scheduler.py b/scripts/autocall_scheduler.py index 961fdb6..4eed0b7 100644 --- a/scripts/autocall_scheduler.py +++ b/scripts/autocall_scheduler.py @@ -603,6 +603,16 @@ def process_queue(conn, settings): if push_call_file(g, settings, attempt_no, is_test_call=is_test, manual=True): mark_queue_processed(conn, q["queue_id"]) pushed += 1 + else: + # Звонить не о чем: окно звонка закрыто (например, заказ готов + # раньше, чем «сегодня + 2 рабочих дня» самовывоза). Закрываем + # заявку навсегда — иначе она молча крутится в очереди, и в + # карточке вечно висит «в очереди, позвоним в течение минуты». + log.warning( + f"Queue item {q['queue_id']} (group {group_id}): окно звонка закрыто, " + "заявка закрыта — роботу нечего предлагать" + ) + mark_queue_processed(conn, q["queue_id"]) attempt_map[group_id] = attempt_no + 1 return pushed diff --git a/src/components/orders/AutocallStatusCard.jsx b/src/components/orders/AutocallStatusCard.jsx index 7f36c3b..1d95781 100644 --- a/src/components/orders/AutocallStatusCard.jsx +++ b/src/components/orders/AutocallStatusCard.jsx @@ -130,9 +130,26 @@ export const AutocallStatusCard = ({ order, userRole }) => { return Date.now() - new Date(queueItem.requested_at).getTime() <= 24 * 60 * 60 * 1000 ? queueItem : null; }, [queueItem]); + // Заявка висит непринятой больше часа в рабочее время — планировщик молча + // пропускает её (окно звонка закрыто: по графику роботу нечего предлагать). + // Показываем честную причину вместо «в очереди». + const callUnreachable = useMemo(() => { + if (!recentQueue || recentQueue.processed_at || callAfter) return false; + const age = Date.now() - new Date(recentQueue.requested_at).getTime(); + if (age < 60 * 60 * 1000) return false; + // Рабочее время сейчас? + const workDays = (settings?.work_days || "").toString().split(",").map((s) => parseInt(s, 10)).filter(Number.isFinite); + const startHour = Number(settings?.work_hours_start ?? 9); + const endHour = Number(settings?.work_hours_end ?? 20); + const msk = new Date(Date.now() + (3 * 60 + new Date().getTimezoneOffset()) * 60000); + const dayOk = workDays.length === 0 || workDays.includes(msk.getDay()); + const hourOk = msk.getHours() >= startHour && msk.getHours() < endHour; + return dayOk && hourOk; + }, [recentQueue, callAfter, settings]); + const process = useMemo( - () => describeCallProcess({ queueItem: recentQueue, callAfter, settings: settings || {} }), - [recentQueue, callAfter, settings], + () => describeCallProcess({ queueItem: recentQueue, callAfter, settings: { ...(settings || {}), call_unreachable: callUnreachable } }), + [recentQueue, callAfter, settings, callUnreachable], ); const processing = process?.phase === "pending" || process?.phase === "pending_later" || process?.phase === "dialing"; @@ -247,6 +264,18 @@ export const AutocallStatusCard = ({ order, userRole }) => { {process.title}
{process.hint}
+ {process.phase === "unreachable" && ( +
+ +
+ )} {process.phase === "done" && callAfter && (
diff --git a/src/services/autocallProcess.js b/src/services/autocallProcess.js index 18f94db..8f2a512 100644 --- a/src/services/autocallProcess.js +++ b/src/services/autocallProcess.js @@ -133,6 +133,19 @@ export const describeCallProcess = ({ queueItem, callAfter, settings = {}, now = const soon = isCallTimeNow({ work_hours_start: startHour, work_hours_end: endHour, work_days: workDays }, now); const ageMinutes = (now.getTime() - new Date(queueItem.requested_at).getTime()) / 60000; + // Заявку взяли в работу, но call-файл не создан: по графику окна звонка для + // этого заказа уже нет (например, окно самовывоза закрылось — заказ готов + // раньше, чем «сегодня + 2 рабочих дня»). Планировщик молча пропускает её + // каждый тик — покажем это честно, а не «в очереди». + if (settings.call_unreachable) { + return { + phase: "unreachable", + tone: "warning", + title: "Звонить по этому заказу больше не о чем", + hint: `Робот каждый раз пропускает заявку: по графику у заказа не осталось доступных дней для предложения — ${requested}. Дальше вручную: позвоните сами или подтвердите день в карточке.`, + }; + } + if (soon) { // В рабочее время заявку обязаны взять на ближайшем тике (раз в минуту). if (ageMinutes > 15) {