diff --git a/scripts/autocall_scheduler.py b/scripts/autocall_scheduler.py index 8bbc84b..e3bbe19 100644 --- a/scripts/autocall_scheduler.py +++ b/scripts/autocall_scheduler.py @@ -73,6 +73,18 @@ def load_settings(conn): } return dict(row) +def resolve_scenario(created_at, now=None): + """Сценарий по аналогии с SMS: в день добавления группы → A (завтра/послезавтра), + со следующего дня → B (день фиксирован = завтра, только половина дня).""" + if not created_at: + return "A" + now = now or datetime.now(timezone.utc) + msk_tz = timezone(timedelta(hours=3)) + created = created_at.astimezone(msk_tz) + current = now.astimezone(msk_tz) + return "A" if created.date() == current.date() else "B" + + def get_groups_to_call(conn, settings): """Группы, которым пора звонить.""" delay_min = settings.get("delay_after_sms_minutes", 90) @@ -84,7 +96,7 @@ def get_groups_to_call(conn, settings): cur.execute(""" SELECT og.id, og.customer_phone, og.customer_phone_normalized, og.notification_status, og.delivery_status, og.sms_sent_at, - og.autocall_opt_out, + og.autocall_opt_out, og.created_at, (SELECT count(*) FROM autocall_log l WHERE l.order_group_id = og.id) as attempt_count, (SELECT count(*) FROM autocall_log l @@ -169,7 +181,9 @@ def build_call_file(group, settings, attempt_no, is_test_call=False): test_phone = normalize_phone(settings.get("test_phone", "9067029953")) dial = "0" + test_phone[1:] if test_phone.startswith("7") else "0" + test_phone - scenario = group.get("scenario", "A") # выбор дня + времени + # Сценарий: в день добавления группы → A (завтра/послезавтра), + # со следующего дня → B (только завтра + половина дня). + scenario = group.get("scenario") or resolve_scenario(group.get("created_at")) call_type = "dlv" lines = [ @@ -232,7 +246,7 @@ def get_queued_calls(conn): ids = tuple(r["order_group_id"] for r in rows if r["order_group_id"]) if ids: cur.execute(""" - SELECT og.id, og.customer_phone, og.customer_phone_normalized + SELECT og.id, og.customer_phone, og.customer_phone_normalized, og.created_at FROM order_groups og WHERE og.id IN %s """, (ids,)) og = {r["id"]: r for r in cur.fetchall()} @@ -241,6 +255,7 @@ def get_queued_calls(conn): if g: r["customer_phone_normalized"] = g.get("customer_phone_normalized") r.setdefault("customer_phone", g.get("customer_phone")) + r.setdefault("created_at", g.get("created_at")) return rows diff --git a/src/components/orders/AutocallStatusCard.jsx b/src/components/orders/AutocallStatusCard.jsx index 24133af..54be81e 100644 --- a/src/components/orders/AutocallStatusCard.jsx +++ b/src/components/orders/AutocallStatusCard.jsx @@ -62,7 +62,7 @@ export const AutocallStatusCard = ({ order, userRole }) => { .select("*") .eq("order_group_id", order.id) .order("created_at", { ascending: false }) - .limit(5); + .limit(10); if (error) throw error; setCalls(data || []); } catch (e) { @@ -78,11 +78,12 @@ export const AutocallStatusCard = ({ order, userRole }) => { if (!order?.id) return; setQueued(true); try { - // Пишем в очередь — планировщик подхватит и создаст call-файл (без ssh из edge fn) + // Пишем в очередь — планировщик подхватит и создаст call-файл (без ssh из edge fn). + // Сценарий планировщик определит сам по дню добавления группы + // (день добавления → A: завтра/послезавтра; позже → B: только завтра). const { error } = await supabase.from("autocall_queue").insert({ order_group_id: order.id, phone: order.customer_phone || order.customerPhone || "", - scenario: "A", }); if (error) throw error; setTimeout(() => setQueued(false), 3000); @@ -137,21 +138,43 @@ export const AutocallStatusCard = ({ order, userRole }) => { )} - {/* Попытки */} + {/* Счётчик попыток — как у SMS «Попыток отправки: N» */} + {calls.length > 0 && ( +
+ Попыток звонков: {calls.length} +
+ )} + + {/* История звонков — как «История SMS» */} {calls.length > 0 ? ( -
- {calls.map((c) => { - const m = OUTCOME_META[c.outcome] || { label: c.outcome, tone: "neutral", icon: "•" }; - const slot = slotText(c); - return ( -
- {fmtTime(c.created_at)} - {m.icon} {m.label} - {slot && {slot}} - {c.was_test_mode && 🧪 тест} -
- ); - })} +
+
История звонков
+
+ {calls.map((c, idx) => { + const m = OUTCOME_META[c.outcome] || { label: c.outcome, tone: "neutral", icon: "•" }; + const slot = slotText(c); + const attemptNo = calls.length - idx; + return ( +
+
+ {fmtTime(c.created_at)} + {m.icon} {m.label} + {c.was_test_mode && 🧪 тест} +
+ {(slot || c.attempt_no > 1 || c.duration_sec > 0) && ( +
+ {c.attempt_no > 1 && Попытка {c.attempt_no}} + {slot && Выбрано: {slot}} + {c.duration_sec > 0 && {Math.round(c.duration_sec)} сек} + {c.scenario === "B" && только завтра} + {c.scenario === "A" && завтра/послезавтра} + {c.changed && клиент правил выбор} +
+ )} +
+ ); + })} +
) : (
Звонков не было
diff --git a/src/components/orders/OrderFilters.jsx b/src/components/orders/OrderFilters.jsx index 738fb3c..7769ec5 100644 --- a/src/components/orders/OrderFilters.jsx +++ b/src/components/orders/OrderFilters.jsx @@ -123,21 +123,21 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities = const smsStatusOptions = [ { value: "all", label: "Статус SMS" }, - { value: "not_sent", label: "SMS: не отправлено" }, - { value: "sent", label: "SMS: отправлено" }, - { value: "delivered", label: "SMS: доставлено" }, - { value: "error", label: "SMS: ошибка" }, + { value: "not_sent", label: "Не отправлено" }, + { value: "sent", label: "Отправлено" }, + { value: "delivered", label: "Доставлено" }, + { value: "error", label: "Ошибка" }, ]; const autocallStatusOptions = [ { value: "all", label: "Статус автодозвона" }, - { value: "not_called", label: "Автодозвон: не звонили" }, - { value: "confirmed", label: "Автодозвон: согласовано" }, - { value: "no_answer", label: "Автодозвон: не дозвонились" }, - { value: "no_input", label: "Автодозвон: не нажал в меню" }, - { value: "manual", label: "Автодозвон: требует ручного управления" }, - { value: "opt_out", label: "Автодозвон: отказ от звонков" }, - { value: "excluded", label: "Автодозвон: не звоним (исключение)" }, + { value: "not_called", label: "Не звонили" }, + { value: "confirmed", label: "Согласовано" }, + { value: "no_answer", label: "Не дозвонились" }, + { value: "no_input", label: "Не нажал в меню" }, + { value: "manual", label: "Требует ручного управления" }, + { value: "opt_out", label: "Отказ от звонков" }, + { value: "excluded", label: "Не звоним (исключение)" }, ]; return (