// MDM SORP — Enrichment Queue (the hero screen). // Keyboard-first: ↑/↓ navigate, A apply, E edit, R reject, D defer, → next. const { useState, useEffect, useMemo, useRef, useCallback: _useCallback } = React; function EnrichmentQueueScreen({ navigate }) { const { t, lang } = useI18n(); const toast = useToast(); const [filter, setFilter] = useState("all"); const [queue, setQueue] = useState(window.DATA.queue); const [activeId, setActiveId] = useState(queue[0]?.id); const [processed, setProcessed] = useState(34); const [editingField, setEditingField] = useState(null); const filtered = useMemo(() => { if (filter === "all") return queue; return queue.filter(q => q.kind === filter); }, [filter, queue]); const active = filtered.find(q => q.id === activeId) || filtered[0]; const goNext = _useCallback((removeCurrent = false) => { const cur = filtered.findIndex(q => q.id === activeId); if (removeCurrent) { setQueue(q => q.filter(x => x.id !== activeId)); setProcessed(p => p + 1); } const nextIdx = removeCurrent ? cur : cur + 1; const next = filtered[nextIdx + (removeCurrent ? 0 : 0)] || filtered[nextIdx + 1]; const remaining = (removeCurrent ? filtered.filter(x => x.id !== activeId) : filtered); setActiveId(remaining[Math.min(cur, remaining.length - 1)]?.id); }, [activeId, filtered]); const apply = () => { toast(`Применено: ${active?.party}`, { onUndo: () => setQueue(window.DATA.queue) }); goNext(true); }; const reject = () => { toast(`Отклонено: ${active?.party}`, { icon: "x", onUndo: () => setQueue(window.DATA.queue) }); goNext(true); }; const defer = () => { toast(`Отложено на 24ч`, { icon: "history" }); goNext(true); }; // Keyboard useEffect(() => { const h = (e) => { if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return; if (e.metaKey || e.ctrlKey) return; if (e.key === "a" || e.key === "A" || e.key === "ф" || e.key === "Ф") { e.preventDefault(); apply(); } else if (e.key === "r" || e.key === "R" || e.key === "к" || e.key === "К") { e.preventDefault(); reject(); } else if (e.key === "d" || e.key === "D" || e.key === "в" || e.key === "В") { e.preventDefault(); defer(); } else if (e.key === "e" || e.key === "E" || e.key === "у" || e.key === "У") { e.preventDefault(); setEditingField("inline"); } else if (e.key === "ArrowDown") { e.preventDefault(); const idx = filtered.findIndex(q => q.id === activeId); setActiveId(filtered[Math.min(filtered.length - 1, idx + 1)]?.id); } else if (e.key === "ArrowUp") { e.preventDefault(); const idx = filtered.findIndex(q => q.id === activeId); setActiveId(filtered[Math.max(0, idx - 1)]?.id); } else if (e.key === "ArrowRight"){ e.preventDefault(); goNext(false); } }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [activeId, filtered, apply, reject, defer, goNext]); const filters = [ { id: "all", label: t("queue_filter_all"), count: queue.length }, { id: "norm", label: t("queue_filter_norm"), count: queue.filter(q => q.kind === "norm").length }, { id: "dedup", label: t("queue_filter_dedup"), count: queue.filter(q => q.kind === "dedup").length }, { id: "ai", label: t("queue_filter_ai"), count: queue.filter(q => q.kind === "ai").length }, { id: "sourcing", label: t("queue_filter_sourcing"), count: queue.filter(q => q.kind === "sourcing").length }, ]; return (
{/* ----- LEFT: list ----- */}

{t("queue_title")}

{queue.length}
{filters.map(f => ( ))}
{filtered.map(q => (
setActiveId(q.id)}>
{q.party}
{labelKind(q.kind, t)} · {q.field} {q.deal && <>·{q.deal}}
{q.raw || "—"} {q.suggested}
))} {filtered.length === 0 && (
{t("empty_queue")}
)}
{/* ----- CENTER: diff stage ----- */}
{active?.party}
{labelKind(active?.kind, t)} {active?.deal && <>· navigate("deal", { id: active.deal })}>{active.deal}} · сфера: {active?.field}
setEditingField(null)} /> {/* Linked records / inline meta */}
Эта запись связана с {active?.deal && {active.deal}} party_id: {active?.party_id || "—"} Изменения уйдут в audit_log с tombstone-возможностью отката.
{t("queue_progress")}: {processed} / {processed + queue.length} {t("queue_speed")}: 3.4с
{/* ----- RIGHT: AI hint panel ----- */}
{t("queue_ai_title")}
{active?.ai && (
{active.ai.title}
{active.ai.body}
)} {/* Related candidates */} {active?.kind === "dedup" && (
Кандидаты на слияние
Schaeffler Middle East FZE
TRN 100229876500003 · Dubai · с 2024-08
Schaffler M.E. Trading LLC
тот же TRN · опечатка в названии · с 2024-12
)} {/* Quality signals */}
Сигналы качества
ИсточникИИ + Asana комментарий
Совпадение типаphone (E.164)
Проверка регуляркиЧастично
Контр. суммаOK
Авторы предыдущих правок
Подтвердила phone · 12 мин назад
Отклонил «вебинар» · вчера
Импорт Amo · 03:14
Горячие клавиши AERD
); } function DiffCard({ active, editing, onClose }) { if (!active) return null; return (
{useI18n().t("queue_field")}
{useI18n().t("raw")}
{useI18n().t("suggested")}
{active.changes.map((c, i) => { const changed = !c.same; return (
{c.field}
{c.note &&
{c.note}
}
{c.raw}
{editing && i === 0 ? e.key === "Escape" && onClose()}/> : {c.sug} } {!c.same && }
); })}
); } function labelKind(k, t) { return ({ norm: t("queue_filter_norm"), dedup: t("queue_filter_dedup"), ai: t("queue_filter_ai"), sourcing: t("queue_filter_sourcing"), })[k] || k; } function aiActionLabel(a) { return ({ apply: "Применить предложение", send_form: "Запросить у исполнителя", review: "Открыть ревью дублей", open_matrix: "Открыть матрицу", })[a] || "Применить"; } window.EnrichmentQueueScreen = EnrichmentQueueScreen;