// MDM SORP — Normalize screen // Очередь нормализации: phone / email / date / currency / address / inn / url // Каждая запись: raw → suggested + buttons accept/edit/skip + источник записи (function () { const { useState, useMemo } = React; const useToast = window.useToast; // ──────────────────────────────────────────────────────────── // Sample data — items needing normalization, mixed by type // ──────────────────────────────────────────────────────────── const SAMPLE = [ { id: 1, type: "phone", entity: "supplier_contacts", row: "CON-001", field: "phone", raw: "8618588031960 / 139 0164 5918", suggested: "+86 18588031960; +86 13901645918", source: "google_sheet", confidence: 0.88 }, { id: 2, type: "phone", entity: "supplier_contacts", row: "CON-014", field: "phone", raw: "+7-916-четыре-нольсем", suggested: "+7 916 ??? ??-??", source: "asana", confidence: 0.32, blocker: "Текст в номере — нужно вручную" }, { id: 3, type: "phone", entity: "supplier_contacts", row: "CON-018", field: "phone", raw: "8 (495) 123 45 67 доб. 215", suggested: "+7 495 123 45 67, доб. 215", source: "amocrm", confidence: 0.93 }, { id: 4, type: "phone", entity: "supplier_contacts", row: "CON-022", field: "phone", raw: "13913291927.0", suggested: "+86 13913291927", source: "google_sheet", confidence: 0.84 }, { id: 5, type: "email", entity: "supplier_contacts", row: "CON-003", field: "email", raw: "erin@huada.com.cn ⏎ huyu@scimee.com.cn", suggested: "erin@huada.com.cn; huyu@scimee.com.cn", source: "google_sheet", confidence: 0.95 }, { id: 6, type: "email", entity: "parties", row: "p_2118", field: "email", raw: "Mr.Sun(at)hetalcorp(dot)com", suggested: "mr.sun@hetalcorp.com", source: "ai_extraction", confidence: 0.79 }, { id: 7, type: "email", entity: "supplier_contacts", row: "CON-031", field: "email", raw: "info@jiangsu-saideli.com,sales@saideli.cn", suggested: "info@jiangsu-saideli.com; sales@saideli.cn", source: "amocrm", confidence: 0.90 }, { id: 8, type: "date", entity: "deals", row: "SORP-3366", field: "kp_deadline", raw: "до конца марта", suggested: "2026-03-31", source: "asana", confidence: 0.65 }, { id: 9, type: "date", entity: "deals", row: "SORP-3201", field: "kp_deadline", raw: "28.06.2026 г.", suggested: "2026-06-28", source: "google_sheet", confidence: 0.99 }, { id: 10, type: "date", entity: "contracts", row: "CT-118", field: "signed_at", raw: "01/04/26", suggested: "2026-04-01", source: "google_sheet", confidence: 0.85, blocker: "Неоднозначно: DD/MM/YY или MM/DD/YY" }, { id: 11, type: "currency", entity: "contracts", row: "CT-118", field: "amount", raw: "6 300 рублей", suggested: { v: 6300, ccy: "RUB" }, source: "google_sheet", confidence: 0.96 }, { id: 12, type: "currency", entity: "deals", row: "SORP-3606", field: "amount", raw: "10000 AED", suggested: { v: 10000, ccy: "AED" }, source: "bank", confidence: 1.0 }, { id: 13, type: "currency", entity: "contracts", row: "CT-119", field: "amount", raw: "8.400.000,00", suggested: { v: 8400000, ccy: "RUB" }, source: "google_sheet", confidence: 0.71, blocker: "Валюта не указана — RUB по контексту" }, { id: 14, type: "address", entity: "parties", row: "p_2002", field: "address", raw: "15F, Tower B, Baoland Plaza, 588 Dalian Road, Shanghai 200082, China", suggested: "200082, China, Shanghai, Yangpu, 588 Dalian Road, Baoland Plaza, Tower B, 15F", source: "ai_extraction", confidence: 0.81 }, { id: 15, type: "address", entity: "parties", row: "p_2115", field: "address", raw: "г. Нижний Новгород, ул. Веденяпина, 21А (Завод Капролактам)", suggested: "603910, Россия, Нижний Новгород, ул. Веденяпина, 21А", source: "amocrm", confidence: 0.87 }, { id: 16, type: "inn", entity: "parties", row: "p_2117", field: "inn", raw: "ИНН 5915001134 / КПП 591501001", suggested: { inn: "5915001134", kpp: "591501001" }, source: "google_sheet", confidence: 0.99 }, { id: 17, type: "url", entity: "supplier_profiles", row: "SUP-001", field: "website", raw: "www.andritz.cn/china-cn", suggested: "https://www.andritz.cn/china-cn", source: "manual", confidence: 1.0 }, ]; const TYPES = { phone: { label: "Телефоны", icon: "parties", color: "#2C5C8C" }, email: { label: "Email", icon: "list", color: "#0F9D58" }, date: { label: "Даты", icon: "calendar",color: "#C77A1F" }, currency: { label: "Суммы", icon: "bar", color: "#6E45E2" }, address: { label: "Адреса", icon: "flag", color: "#A1242E" }, inn: { label: "ИНН/КПП", icon: "shield", color: "#7A7468" }, url: { label: "URL", icon: "link", color: "#1E5E40" }, }; const SRC_LABELS = { amocrm: "AmoCRM", asana: "Asana", google_sheet: "Sheet", bank: "Банк", manual: "Вручную", ai_extraction: "AI", drive: "Drive", }; // ──────────────────────────────────────────────────────────── // Main screen // ──────────────────────────────────────────────────────────── function NormalizeScreen() { const [items, setItems] = useState(SAMPLE); const [activeType, setActiveType] = useState("all"); const [showResolved, setShowResolved] = useState(false); const [editId, setEditId] = useState(null); const [editVal, setEditVal] = useState(""); const toast = useToast(); const counts = useMemo(() => { const c = { all: items.filter(i => !i.resolved).length }; for (const t of Object.keys(TYPES)) c[t] = items.filter(i => i.type === t && !i.resolved).length; return c; }, [items]); const filtered = useMemo(() => items.filter(i => (showResolved || !i.resolved) && (activeType === "all" || i.type === activeType)), [items, activeType, showResolved] ); const accept = (id) => { setItems(prev => prev.map(i => i.id === id ? { ...i, resolved: true, resolvedAs: "accepted", resolvedAt: new Date() } : i)); toast("Принято · применено к записи", { icon: "check", onUndo: () => setItems(SAMPLE) }); }; const skip = (id) => { setItems(prev => prev.map(i => i.id === id ? { ...i, resolved: true, resolvedAs: "skipped", resolvedAt: new Date() } : i)); }; const startEdit = (item) => { setEditId(item.id); setEditVal(typeof item.suggested === "string" ? item.suggested : JSON.stringify(item.suggested)); }; const saveEdit = () => { setItems(prev => prev.map(i => i.id === editId ? { ...i, resolved: true, resolvedAs: "edited", suggested: editVal, resolvedAt: new Date() } : i)); setEditId(null); setEditVal(""); toast("Сохранено", { icon: "check" }); }; const bulkAcceptHighConfidence = () => { const toAccept = items.filter(i => !i.resolved && i.confidence >= 0.9); if (!toAccept.length) { toast("Нет записей с confidence ≥ 90%"); return; } setItems(prev => prev.map(i => toAccept.find(t => t.id === i.id) ? { ...i, resolved: true, resolvedAs: "bulk_accepted" } : i)); toast(`Принято ${toAccept.length} записей с confidence ≥ 90%`, { icon: "sparkle", onUndo: () => setItems(SAMPLE) }); }; return (
{item.entity}.{item.field}
→ {item.row}
{SRC_LABELS[item.source] || item.source}
= 70 ? "mid" : "low")}>
{conf}%
{item.blocker && (