feat(autocall): история звонков в карточке — как у SMS (попытки, слот, сценарий, длительность)
This commit is contained in:
parent
04f78f7367
commit
c7a35cdd29
|
|
@ -73,6 +73,18 @@ def load_settings(conn):
|
||||||
}
|
}
|
||||||
return dict(row)
|
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):
|
def get_groups_to_call(conn, settings):
|
||||||
"""Группы, которым пора звонить."""
|
"""Группы, которым пора звонить."""
|
||||||
delay_min = settings.get("delay_after_sms_minutes", 90)
|
delay_min = settings.get("delay_after_sms_minutes", 90)
|
||||||
|
|
@ -84,7 +96,7 @@ def get_groups_to_call(conn, settings):
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT og.id, og.customer_phone, og.customer_phone_normalized,
|
SELECT og.id, og.customer_phone, og.customer_phone_normalized,
|
||||||
og.notification_status, og.delivery_status, og.sms_sent_at,
|
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
|
(SELECT count(*) FROM autocall_log l
|
||||||
WHERE l.order_group_id = og.id) as attempt_count,
|
WHERE l.order_group_id = og.id) as attempt_count,
|
||||||
(SELECT count(*) FROM autocall_log l
|
(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"))
|
test_phone = normalize_phone(settings.get("test_phone", "9067029953"))
|
||||||
dial = "0" + test_phone[1:] if test_phone.startswith("7") else "0" + test_phone
|
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"
|
call_type = "dlv"
|
||||||
|
|
||||||
lines = [
|
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"])
|
ids = tuple(r["order_group_id"] for r in rows if r["order_group_id"])
|
||||||
if ids:
|
if ids:
|
||||||
cur.execute("""
|
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
|
FROM order_groups og WHERE og.id IN %s
|
||||||
""", (ids,))
|
""", (ids,))
|
||||||
og = {r["id"]: r for r in cur.fetchall()}
|
og = {r["id"]: r for r in cur.fetchall()}
|
||||||
|
|
@ -241,6 +255,7 @@ def get_queued_calls(conn):
|
||||||
if g:
|
if g:
|
||||||
r["customer_phone_normalized"] = g.get("customer_phone_normalized")
|
r["customer_phone_normalized"] = g.get("customer_phone_normalized")
|
||||||
r.setdefault("customer_phone", g.get("customer_phone"))
|
r.setdefault("customer_phone", g.get("customer_phone"))
|
||||||
|
r.setdefault("created_at", g.get("created_at"))
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export const AutocallStatusCard = ({ order, userRole }) => {
|
||||||
.select("*")
|
.select("*")
|
||||||
.eq("order_group_id", order.id)
|
.eq("order_group_id", order.id)
|
||||||
.order("created_at", { ascending: false })
|
.order("created_at", { ascending: false })
|
||||||
.limit(5);
|
.limit(10);
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
setCalls(data || []);
|
setCalls(data || []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -78,11 +78,12 @@ export const AutocallStatusCard = ({ order, userRole }) => {
|
||||||
if (!order?.id) return;
|
if (!order?.id) return;
|
||||||
setQueued(true);
|
setQueued(true);
|
||||||
try {
|
try {
|
||||||
// Пишем в очередь — планировщик подхватит и создаст call-файл (без ssh из edge fn)
|
// Пишем в очередь — планировщик подхватит и создаст call-файл (без ssh из edge fn).
|
||||||
|
// Сценарий планировщик определит сам по дню добавления группы
|
||||||
|
// (день добавления → A: завтра/послезавтра; позже → B: только завтра).
|
||||||
const { error } = await supabase.from("autocall_queue").insert({
|
const { error } = await supabase.from("autocall_queue").insert({
|
||||||
order_group_id: order.id,
|
order_group_id: order.id,
|
||||||
phone: order.customer_phone || order.customerPhone || "",
|
phone: order.customer_phone || order.customerPhone || "",
|
||||||
scenario: "A",
|
|
||||||
});
|
});
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
setTimeout(() => setQueued(false), 3000);
|
setTimeout(() => setQueued(false), 3000);
|
||||||
|
|
@ -137,22 +138,44 @@ export const AutocallStatusCard = ({ order, userRole }) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Попытки */}
|
{/* Счётчик попыток — как у SMS «Попыток отправки: N» */}
|
||||||
|
{calls.length > 0 && (
|
||||||
|
<div className="mb-2 text-[11px] text-[var(--color-text-muted)]">
|
||||||
|
Попыток звонков: {calls.length}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* История звонков — как «История SMS» */}
|
||||||
{calls.length > 0 ? (
|
{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">
|
<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 m = OUTCOME_META[c.outcome] || { label: c.outcome, tone: "neutral", icon: "•" };
|
||||||
const slot = slotText(c);
|
const slot = slotText(c);
|
||||||
|
const attemptNo = calls.length - idx;
|
||||||
return (
|
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>
|
<span className="text-[var(--color-text-muted)]">{fmtTime(c.created_at)}</span>
|
||||||
<Badge tone={m.tone}>{m.icon} {m.label}</Badge>
|
<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>}
|
{c.was_test_mode && <span className="text-[10px] text-[var(--color-warning)]">🧪 тест</span>}
|
||||||
</div>
|
</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>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-[var(--color-text-muted)]">Звонков не было</div>
|
<div className="text-xs text-[var(--color-text-muted)]">Звонков не было</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -123,21 +123,21 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
||||||
|
|
||||||
const smsStatusOptions = [
|
const smsStatusOptions = [
|
||||||
{ value: "all", label: "Статус SMS" },
|
{ value: "all", label: "Статус SMS" },
|
||||||
{ value: "not_sent", label: "SMS: не отправлено" },
|
{ value: "not_sent", label: "Не отправлено" },
|
||||||
{ value: "sent", label: "SMS: отправлено" },
|
{ value: "sent", label: "Отправлено" },
|
||||||
{ value: "delivered", label: "SMS: доставлено" },
|
{ value: "delivered", label: "Доставлено" },
|
||||||
{ value: "error", label: "SMS: ошибка" },
|
{ value: "error", label: "Ошибка" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const autocallStatusOptions = [
|
const autocallStatusOptions = [
|
||||||
{ value: "all", label: "Статус автодозвона" },
|
{ value: "all", label: "Статус автодозвона" },
|
||||||
{ value: "not_called", label: "Автодозвон: не звонили" },
|
{ value: "not_called", label: "Не звонили" },
|
||||||
{ value: "confirmed", label: "Автодозвон: согласовано" },
|
{ value: "confirmed", label: "Согласовано" },
|
||||||
{ value: "no_answer", label: "Автодозвон: не дозвонились" },
|
{ value: "no_answer", label: "Не дозвонились" },
|
||||||
{ value: "no_input", label: "Автодозвон: не нажал в меню" },
|
{ value: "no_input", label: "Не нажал в меню" },
|
||||||
{ value: "manual", label: "Автодозвон: требует ручного управления" },
|
{ value: "manual", label: "Требует ручного управления" },
|
||||||
{ value: "opt_out", label: "Автодозвон: отказ от звонков" },
|
{ value: "opt_out", label: "Отказ от звонков" },
|
||||||
{ value: "excluded", label: "Автодозвон: не звоним (исключение)" },
|
{ value: "excluded", label: "Не звоним (исключение)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue