feat(autocall): показ последних тест-звонков + честный статус задержки планировщика
This commit is contained in:
parent
0d8e0f9d40
commit
5e4bfc1af0
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue