feat(autocall): история звонков в карточке — как у SMS (попытки, слот, сценарий, длительность)
This commit is contained in:
parent
04f78f7367
commit
c7a35cdd29
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,22 +138,44 @@ export const AutocallStatusCard = ({ order, userRole }) => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Попытки */}
|
||||
{/* Счётчик попыток — как у SMS «Попыток отправки: N» */}
|
||||
{calls.length > 0 && (
|
||||
<div className="mb-2 text-[11px] text-[var(--color-text-muted)]">
|
||||
Попыток звонков: {calls.length}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* История звонков — как «История SMS» */}
|
||||
{calls.length > 0 ? (
|
||||
<div className="mt-1 border-t border-[var(--color-border)] pt-3">
|
||||
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">История звонков</div>
|
||||
<div className="space-y-1.5">
|
||||
{calls.map((c) => {
|
||||
{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 (
|
||||
<div key={c.id} className="flex items-center gap-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] px-2.5 py-1.5 text-[11px]">
|
||||
<div key={c.id} className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] px-2.5 py-1.5">
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px]">
|
||||
<span className="text-[var(--color-text-muted)]">{fmtTime(c.created_at)}</span>
|
||||
<Badge tone={m.tone}>{m.icon} {m.label}</Badge>
|
||||
{slot && <span className="text-[var(--color-text)]">{slot}</span>}
|
||||
{c.was_test_mode && <span className="text-[10px] text-[var(--color-warning)]">🧪 тест</span>}
|
||||
</div>
|
||||
{(slot || c.attempt_no > 1 || c.duration_sec > 0) && (
|
||||
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[10px] text-[var(--color-text-muted)]">
|
||||
{c.attempt_no > 1 && <span>Попытка {c.attempt_no}</span>}
|
||||
{slot && <span>Выбрано: {slot}</span>}
|
||||
{c.duration_sec > 0 && <span>{Math.round(c.duration_sec)} сек</span>}
|
||||
{c.scenario === "B" && <span>только завтра</span>}
|
||||
{c.scenario === "A" && <span>завтра/послезавтра</span>}
|
||||
{c.changed && <span className="text-[var(--color-warning)]">клиент правил выбор</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-[var(--color-text-muted)]">Звонков не было</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
Loading…
Reference in New Issue