fix(autocall): заявка «позвонить сейчас» с закрытым окном звонка больше не висит вечно в очереди — планировщик закрывает её, карточка честно говорит «звонить больше не о чем» + кнопка «всё равно позвонить»

This commit is contained in:
Hermes 2026-09-16 16:39:28 +02:00
parent b965216181
commit dacd43165c
3 changed files with 54 additions and 2 deletions

View File

@ -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

View File

@ -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 }) => {
<span>{process.title}</span>
</div>
<div className="mt-0.5 text-[10px] opacity-80">{process.hint}</div>
{process.phase === "unreachable" && (
<div className="mt-1.5">
<button
type="button"
onClick={handleCallNow}
disabled={sending || processing}
className="rounded-lg border border-[var(--color-warning)] px-2.5 py-1 text-[11px] font-medium text-[var(--color-warning)] hover:bg-[rgba(245,158,11,0.08)] disabled:opacity-50"
>
{sending ? "Отправляем заявку…" : "📞 Всё равно позвонить"}
</button>
</div>
)}
{process.phase === "done" && callAfter && (
<div className="mt-1.5 flex flex-wrap items-center gap-2">
<Badge tone={(OUTCOME_META[callAfter.outcome] || {}).tone || "neutral"}>

View File

@ -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) {