feat(autocall): показ последних тест-звонков + честный статус задержки планировщика

This commit is contained in:
Hermes 2026-09-02 11:37:16 +02:00
parent 0d8e0f9d40
commit 5e4bfc1af0
1 changed files with 46 additions and 3 deletions

View File

@ -52,8 +52,25 @@ export const TestCallPanel = () => {
const [status, setStatus] = useState("idle"); // idle | queueing | queued | dialing | done | error const [status, setStatus] = useState("idle"); // idle | queueing | queued | dialing | done | error
const [result, setResult] = useState(null); // последняя строка autocall_log для этого теста const [result, setResult] = useState(null); // последняя строка autocall_log для этого теста
const [startedAt, setStartedAt] = useState(null); const [startedAt, setStartedAt] = useState(null);
const [lastTests, setLastTests] = useState([]); // последние тест-звонки (для контекста)
const pollRef = useRef(null); const pollRef = useRef(null);
// При монтировании показываем последние тестовые звонки,
// чтобы было видно, что система живая и что было в прошлый раз.
const loadLastTests = useCallback(async () => {
try {
const { data, error } = await supabase
.from("autocall_log")
.select("*")
.eq("was_test_mode", true)
.order("created_at", { ascending: false })
.limit(3);
if (!error && data) setLastTests(data);
} catch { /* глушим */ }
}, []);
useEffect(() => { loadLastTests(); }, [loadLastTests]);
const stopPolling = useCallback(() => { const stopPolling = useCallback(() => {
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
}, []); }, []);
@ -77,9 +94,9 @@ export const TestCallPanel = () => {
}); });
if (error) throw error; if (error) throw error;
setStatus("queued"); setStatus("queued");
setResult({ outcome: "queued", note: "Запрос поставлен в очередь. Планировщик создаст звонок." }); setResult({ outcome: "queued", note: "Запрос в очереди. Планировщик обрабатывает её раз в ~5 минут — ожидайте звонок." });
// Начинаем поллить результат по нашему номеру с момента запуска // Начинаем поллить результат по нашему номеру с момента запуска
pollRef.current = setInterval(() => pollResult(now), 3000); pollRef.current = setInterval(() => pollResult(now), 4000);
} catch (e) { } catch (e) {
console.error("Test call queue error:", e); console.error("Test call queue error:", e);
setStatus("error"); setStatus("error");
@ -107,6 +124,7 @@ export const TestCallPanel = () => {
// Как только есть финальный исход (не in_progress) — стоп // Как только есть финальный исход (не in_progress) — стоп
if (["confirmed","confirmed_kept_previous","confirmed_silent","no_answer","no_input_day","no_input_half","operator","opt_out","invalid_number","error"].includes(row.outcome)) { if (["confirmed","confirmed_kept_previous","confirmed_silent","no_answer","no_input_day","no_input_half","operator","opt_out","invalid_number","error"].includes(row.outcome)) {
stopPolling(); stopPolling();
loadLastTests();
} }
} else { } else {
setStatus((s) => (s === "dialing" ? "dialing" : "queued")); setStatus((s) => (s === "dialing" ? "dialing" : "queued"));
@ -115,7 +133,7 @@ export const TestCallPanel = () => {
// глуши, поллинг продолжит // глуши, поллинг продолжит
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [phone, stopPolling]); }, [phone, stopPolling, loadLastTests]);
useEffect(() => () => stopPolling(), [stopPolling]); useEffect(() => () => stopPolling(), [stopPolling]);
@ -193,6 +211,31 @@ export const TestCallPanel = () => {
)} )}
</div> </div>
)} )}
{/* ── Последние тест-звонки (контекст: что система уже делала) ───────── */}
{lastTests.length > 0 && (
<div className="mt-3">
<div className="mb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-[var(--color-text-muted)]">
Последние тест-звонки
</div>
<div className="space-y-1">
{lastTests.map((t) => {
const tm = OUTCOME_META[t.outcome] || OUTCOME_META.error;
const slot = t.dtmf_day || t.dtmf_half
? `${DAY_LABEL[t.dtmf_day] || ""}${t.dtmf_day && t.dtmf_half ? ", " : ""}${HALF_LABEL[t.dtmf_half] || ""}`.trim() || "—"
: null;
return (
<div key={t.id} className="flex flex-wrap items-center gap-x-3 gap-y-0.5 rounded-lg bg-[var(--color-surface)] px-2 py-1.5 text-[11px]">
<span className="font-medium text-[var(--color-text)]">{fmtPhone(t.customer_phone)}</span>
<span className={`text-[var(--color-text-muted)]`}>{tm.icon} {tm.label}</span>
{slot && <span className="text-[var(--color-text-muted)]">· {slot}</span>}
<span className="text-[10px] text-[var(--color-text-muted)]">· {fmtTime(t.created_at)}</span>
</div>
);
})}
</div>
</div>
)}
</Panel> </Panel>
); );
}; };