// MDM SORP — Parties grid + Party card. const { useState, useEffect, useMemo, useRef, useCallback, createContext, useContext } = React; function PartiesScreen({ navigate }) { const { t, lang } = useI18n(); const rows = useMemo(() => window.DATA.parties.map(p => ({ ...p, name: p.short, full: p.full, roles_arr: p.roles || [], taxId: p.inn || p.trn, source_badge: p.source, })), []); const roleOpts = [ { value: "customer", label: "Заказчик", color: "red" }, { value: "supplier", label: "Поставщик", color: "blue" }, { value: "end_user", label: "Конечник", color: "orange" }, { value: "vehicle", label: "Заход", color: "ink" }, { value: "contact", label: "Контакт", color: "gray" }, { value: "beneficiary", label: "Бенефициар", color: "purple" }, ]; const countryOpts = [ { value: "RU", label: "Россия", color: "red" }, { value: "AE", label: "ОАЭ", color: "green" }, { value: "CN", label: "Китай", color: "yellow" }, { value: "TR", label: "Турция", color: "orange" }, { value: "DE", label: "Германия", color: "gray" }, ]; const sanctionsOpts = [ { value: "clean", label: "Чисто", color: "green" }, { value: "review", label: "Review", color: "yellow" }, { value: "match", label: "Совпадение", color: "orange" }, { value: "flag", label: "Flag", color: "red" }, ]; const sourceOpts = [ { value: "amo", label: "AmoCRM", color: "blue" }, { value: "asana", label: "Asana", color: "red" }, { value: "drive", label: "Drive", color: "blue" }, { value: "ai", label: "ИИ", color: "red" }, { value: "dadata", label: "Dadata", color: "purple" }, { value: "manual", label: "Ручной", color: "gray" }, ]; const columns = [ { id: "id", label: "ID", type: "text", width: 90, mono: true }, { id: "name", label: "Краткое имя", type: "primary", width: 240 }, { id: "full", label: "Полное наименование", type: "text", width: 280 }, { id: "roles_arr", label: "Роли", type: "multi_select", width: 200, options: roleOpts }, { id: "taxId", label: "ИНН / TRN", type: "text", width: 160, mono: true }, { id: "country", label: "Страна", type: "select", width: 150, options: countryOpts }, { id: "city", label: "Город", type: "text", width: 160 }, { id: "phone", label: "Телефон", type: "text", width: 170, mono: true }, { id: "email", label: "E-mail", type: "text", width: 220 }, { id: "screening", label: "Скрининг", type: "sanctions", width: 130, options: sanctionsOpts }, { id: "source_badge", label: "Источник", type: "select", width: 130, options: sourceOpts }, { id: "conf", label: "Conf.", type: "percent", width: 80, accessor: r => Math.round((r.conf || 0) * 100) }, ]; const views = [ { name: "Все party", kind: "grid", count: rows.length }, { name: "Поставщики", kind: "grid", filter: [{ field: "roles_arr", op: "contains", value: "supplier" }] }, { name: "По роли", kind: "kanban", stackBy: "country" }, { name: "Карты поставщиков", kind: "gallery", filter: [{ field: "roles_arr", op: "contains", value: "supplier" }] }, { name: "Низкий confidence", kind: "grid", filter: [{ field: "conf", op: "lt", value: 50 }], colorBy: "screening" }, { name: "Группа по стране", kind: "grid", groupBy: "country", sort: [{ field: "name", dir: "asc" }] }, ]; return ( navigate("party", { id: r.id })} /> ); } function PartyDrawer({ p, onOpen }) { const { t, lang } = useI18n(); if (!p) return null; return ( <>
{p.short}
{p.full}
{(p.roles || []).map(r => )} {p.city}

{lang === "ru" ? "Связанные сделки" : "Related deals"}

{window.DATA.deals.filter(d => d.customer === p.id || d.end_user === p.id || d.vehicle === p.id).slice(0, 3).map(d => (
{d.id} {d.title} {d.stage} {fmtMoney(d.value_usd, "USD")}
))}
); } function PartyFields({ p }) { const { t } = useI18n(); const rows = [ { lbl: t("party_full_name"), v: p.full, src: p.source, conf: p.conf, raw: p.raw_note }, { lbl: t("party_short_name"), v: p.short, src: p.source, conf: p.conf }, { lbl: p.inn ? t("party_inn") : t("party_trn"), v: p.inn || p.trn || "—", src: p.inn ? "amo" : "manual", conf: 1.0, mono: true }, { lbl: t("party_country"), v: p.country, src: p.source, conf: 1.0 }, { lbl: t("party_phone"), v: p.phone, src: p.source, conf: p.conf, mono: true }, { lbl: t("party_email"), v: p.email, src: p.source, conf: p.conf }, { lbl: t("party_address"), v: p.city, src: "dadata", conf: 0.88 }, ]; return (
{rows.map((r, i) => (
{r.lbl}
{r.v || "—"}
))}
); } /* ===================== PARTY CARD (full) ===================== */ function PartyCardScreen({ navigate, params }) { const { t, lang } = useI18n(); const id = params?.id || "p_2002"; const p = findParty(id) || window.DATA.parties[0]; const [tab, setTab] = useState("overview"); return (
· {p.id} {p.possibleDupOf && ( <> · возможный дубль · navigate("dedup")} style={{ color: "inherit", textDecoration: "underline", cursor: "pointer" }}>открыть ревью )}

{p.short}

{p.full}
{(p.roles || []).map(r => )} {p.city} {p.inn && ИНН {p.inn}} {p.trn && TRN {p.trn}}
{[ { id: "overview", label: t("overview"), n: null }, { id: "deals", label: t("deals"), n: window.DATA.deals.filter(d => d.customer === p.id || d.end_user === p.id || d.vehicle === p.id).length }, { id: "contacts", label: lang === "ru" ? "Контакты" : "Contacts", n: 4 }, { id: "docs", label: t("docs"), n: 12 }, { id: "capability", label: lang === "ru" ? "Возможности" : "Capability", n: p.roles?.includes("supplier") ? 6 : null }, { id: "history", label: t("history"), n: 28 }, ].filter(x => x.n !== null || x.id === "overview").map(x => )}
{tab === "overview" && } {tab === "deals" && } {tab === "contacts" && } {tab === "docs" && } {tab === "capability" && } {tab === "history" && }
); } function PartyOverviewTab({ p }) { const { t, lang } = useI18n(); return (

{lang === "ru" ? "Основные поля" : "Core fields"}

fill-rate 87%

{lang === "ru" ? "Связи в системах" : "System links"}

{lang === "ru" ? "Скрининг" : "Screening"}

OpenSanctions · 2026-05-27 03:21
OFAC SDNClean
EU CFSPClean
UK HM TreasuryClean
Dual-use HS 8482Advisory

{lang === "ru" ? "Метаданные" : "Metadata"}

Создано2024-08-14
Обновлено2026-05-26 14:08
СоздалService · импорт Amo
ДоступRBAC: открытое
); } function PartyDealsTab({ p, navigate }) { const deals = window.DATA.deals.filter(d => d.customer === p.id || d.end_user === p.id || d.vehicle === p.id); if (!deals.length) return
Связанных сделок нет.
; return (
{deals.map(d => ( navigate("deal", { id: d.id })}> ))}
IDСделкаРоль partyЭтапСуммаДедлайн
{d.id} {d.title} {d.customer === p.id ? : d.end_user === p.id ? : } {d.stage} {fmtMoney(d.value_usd, "USD")} {d.deadline}
); } function PartyContactsTab() { const contacts = [ { name: "Mohammed Al-Fardan", role: "Sales Manager ME", phone: "+971 50 412-7788", email: "m.alfardan@schaeffler.com", src: "amo", conf: 0.96 }, { name: "Priya Ramesh", role: "Logistics Coordinator", phone: "+971 4 805-1903", email: "p.ramesh@schaeffler.com", src: "drive", conf: 0.78, raw: "из PDF КП от 2026-04-22" }, { name: "Sven Karlsson", role: "Global Key Account", phone: "+49 9132 82-0", email: "sven.karlsson@schaeffler.com", src: "ai", conf: 0.61 }, { name: "—", role: "(нужен бенефициар)", phone: "", email: "", src: "manual", conf: 0.0 }, ]; return (
{contacts.map((c, i) => ( ))}
ИмяРольТелефонE-mailИсточник
{c.name} {c.role} {c.phone || } {c.email || } {c.conf > 0 ? : пусто}
); } function PartyDocsTab() { return
Связанные документы из Drive (12 файлов) — см. вкладку Документы сделки.
; } function PartyCapabilityTab() { const caps = [ { cat: "Подшипники качения (HS 8482.10)", confidence: 0.96, lead: "28–35 д", note: "Прямой производитель, JAFZA склад" }, { cat: "Подшипники конические (HS 8482.20)", confidence: 0.92, lead: "35–42 д", note: "Каталог 23/24" }, { cat: "Подшипники самовыравнивающие (HS 8482.30)", confidence: 0.85, lead: "28 д", note: "—" }, { cat: "Уплотнения промышленные", confidence: 0.74, lead: "42 д", note: "Доп. категория, поставка под заказ" }, { cat: "Смазочные системы", confidence: 0.61, lead: "—", note: "ИИ-предложение, не подтверждено" }, { cat: "Гидравлические клапаны (HS 8481)", confidence: 0.20, lead: "—", note: "Не профильно, отклонить?" }, ]; return (

Категории поставки (capability matrix)

{caps.map((c, i) => (
{c.cat}
{c.note}
0.8 ? "var(--status-success)" : c.confidence > 0.5 ? "var(--status-warning)" : "var(--sorp-red)" }}/>
{Math.round(c.confidence * 100)}%
{c.lead}
))}
); } function PartyHistoryTab() { return
История изменений party — формат как на сделке.
; } window.PartiesScreen = PartiesScreen; window.PartyCardScreen = PartyCardScreen;