// MDM SORP — Dashboards (M12). const { useState, useEffect, useMemo, useRef, useCallback, createContext, useContext } = React; function DashboardScreen({ navigate }) { const { t, lang } = useI18n(); const [view, setView] = useState("director"); // director | procurement | compliance | dq return (

{lang === "ru" ? "Аналитика · Metabase" : "Analytics · Metabase"}

{[ { id: "director", label: lang === "ru" ? "Руководитель" : "Director" }, { id: "procurement", label: lang === "ru" ? "Закупки" : "Procurement" }, { id: "compliance", label: lang === "ru" ? "Комплаенс" : "Compliance" }, { id: "dq", label: lang === "ru" ? "Качество данных" : "Data quality" }, ].map(x => ( ))}
{view === "director" && } {view === "procurement" && } {view === "compliance" && } {view === "dq" && }
); } function DirectorBoard() { return (
Активные сделки
38
+4
против пр. недели
Закупочный бюджет
$ 4.2M
+12%
USD, по курсу ЦБ
Средняя маржа
14.8%
−0.6п
цель: 16%
Прогноз поставки
92%
+3п
в срок к дедлайну

Воронка заявок · ₽ млн в каждом этапе

drill-down: клик по столбцу → список сделок

Маржа по направлениям

YTD
{[ { name: "Подшипники", mar: 14.6, val: 1820 }, { name: "Гидравлика", mar: 17.2, val: 940 }, { name: "ЧПУ запчасти", mar: 22.4, val: 410 }, { name: "Метизы", mar: 26.5, val: 280 }, { name: "Редукторы", mar: 11.8, val: 750 }, ].map((b, i) => (
{b.name}
20 ? "var(--status-success)" : b.mar > 15 ? "var(--sorp-red)" : "var(--status-warning)", borderRadius: 3 }}/>
{b.mar}%
${b.val}k
))}

Тренд: маржа vs закупочный бюджет

12 недель

Дедлайны · 14 дней

«успеем»/«риск»/«просрочка»
{window.DATA.deals.slice(0, 5).map(d => { const days = Math.round((new Date(d.deadline) - new Date("2026-05-27")) / 86400000); const tone = days > 14 ? "success" : days > 0 ? "warning" : "danger"; return (
{d.id} {d.title} {days > 0 ? `${days}д` : "просрочка"} {d.deadline}
); })}
); } function ProcurementBoard() { return (
Поставщиков активных
68
+4 за месяц
Открытые RFQ
14
5 ждут ответа >48ч
Ср. lead time
34 д
оптимум: 30
Себест. логистика+таможня
7.8%
от закупки в RUB

Поставщики по объёму ($) · топ-10

{[ { name: "SKF Middle East FZE", c: "ae", v: 982, deals: 14, lead: 32, sanc: "clean" }, { name: "Schaeffler Middle East FZE", c: "ae", v: 740, deals: 11, lead: 35, sanc: "clean" }, { name: "Wuxi WLR Bearings", c: "cn", v: 412, deals: 8, lead: 28, sanc: "clean" }, { name: "Erkunt Sanayi A.Ş.", c: "tr", v: 286, deals: 4, lead: 42, sanc: "clean" }, { name: "Bosch Rexroth ME", c: "ae", v: 184, deals: 3, lead: 30, sanc: "clean" }, { name: "Siemens (Shanghai) Industrial", c: "cn", v: 91, deals: 2, lead: 45, sanc: "review" }, ].map((s, i) => (
{s.name}
${s.v}k {s.deals} сд. {s.lead}д
))}

Мультивалюта закупок · доля по сумме

Курс/наценка

2026-05-27 · ЦБ РФ + 1.5%
{[ { ccy: "USD", rate: 78.42, d: "+0.18" }, { ccy: "EUR", rate: 84.10, d: "+0.32" }, { ccy: "AED", rate: 21.35, d: "+0.05" }, { ccy: "CNY", rate: 10.92, d: "−0.02" }, ].map((r, i) => (
{r.ccy} {r.rate} ₽ {r.d}
))}
); } function ComplianceBoard() { return (
Записей в базе
2 184
party — все роли
Под скринингом
2 184
OpenSanctions ежедневно
Совпадений
3
1 review · 2 false-positive
Dual-use позиции
27
HS 8413/8481/8542

Активные комплаенс-флаги

требуют решения
ЧтоГдеКакой списокИсточник флагаПрим.
Siemens (Shanghai) Industrial Trading Поставщик · 2 сделки EAR99 advisory Связь с подсанкционной материнской
SORP-3955 · позиция 04 deal_item Dual-use HS 8482.10 Подшипник 7224 — подшипники для прокатных станов
p_2003 Schaffler M.E. Trading Поставщик Низкое доверие · возможный фейк/дубль

Санкционная экспозиция портфеля

по сумме сделок

Конечник ≠ заказчику

parallel-import scenario
SORP-3974 · Vesuvius rolls Северсталь-Метиз ТМК Волжский
SORP-4011 ООО «Холдинг-Запад» 3 разных АО
Всего 9 сделок · «Конечник» обязателен по комплаенс-политике для проверки скрытого назначения.
); } function DataQualityBoard({ navigate }) { return (
Fill-rate party
84%
+1.2п
Дубли (кандидаты)
4
−1
Ненормализованных
62
телефоны · email · адреса
Записей без источника
11
требует ручной разметки

Fill-rate по полям party

из 2 184 записей
{[ { f: "ИНН / TRN", v: 96 }, { f: "Полное имя", v: 99 }, { f: "Телефон", v: 88 }, { f: "E-mail", v: 79 }, { f: "Адрес", v: 71 }, { f: "Бенефициар", v: 28 }, { f: "Координаты", v: 64 }, { f: "Капабилити", v: 42 }, ].map((r, i) => (
{r.f}
80 ? "var(--status-success)" : r.v > 60 ? "var(--status-warning)" : "var(--sorp-red)", borderRadius: 3 }}/>
{r.v}%
))}

Источники данных

в каких полях
{[ { s: "Amo", pct: 42, color: "#2E91FE" }, { s: "Asana", pct: 28, color: "#F06A6A" }, { s: "Drive (OCR)", pct: 12, color: "#1A73E8" }, { s: "ИИ-обогащение", pct: 10, color: "#D63D4A" }, { s: "Ручной ввод", pct: 7, color: "#3A4049" }, { s: "Dadata", pct: 1, color: "#6E45E2" }, ].map((r, i) => (
{r.s} {r.pct}%
))}
); } /* ---------- Mini chart components (pure SVG) ---------- */ function FunnelChart() { const stages = [ { name: "Заявка", v: 4.2, n: 28 }, { name: "Поиск", v: 8.1, n: 18 }, { name: "Спец.", v: 6.6, n: 14 }, { name: "Закупка", v: 5.2, n: 9 }, { name: "Поставка", v: 3.4, n: 7 }, { name: "Завершено",v: 2.1, n: 5 }, ]; const max = Math.max(...stages.map(s => s.v)); return (
{stages.map((s, i) => { const h = s.v / max * 180; return (
{s.v}M ₽
{s.name}
{s.n} сделок
); })}
); } function LineChart() { const w = 480, h = 200, pad = 24; const data = [12.4, 14.1, 13.8, 15.2, 14.9, 16.1, 15.4, 14.2, 13.6, 15.5, 16.8, 14.8]; const max = Math.max(...data), min = Math.min(...data); const pts = data.map((v, i) => [pad + i / (data.length - 1) * (w - 2 * pad), h - pad - (v - min) / (max - min) * (h - 2 * pad)]); const d = pts.map((p, i) => (i === 0 ? "M" : "L") + p[0].toFixed(1) + "," + p[1].toFixed(1)).join(" "); return ( {pts.map((p, i) => )} {pts.map((p, i) => i % 2 === 0 ? W{i + 12} : null)} ); } function DonutChart({ segments }) { const total = segments.reduce((s, x) => s + x.v, 0); let a = -Math.PI / 2; const cx = 60, cy = 60, r = 50, r2 = 32; const paths = segments.map((s) => { const start = a; const len = s.v / total * Math.PI * 2; a += len; const x1 = cx + r * Math.cos(start), y1 = cy + r * Math.sin(start); const x2 = cx + r * Math.cos(start + len), y2 = cy + r * Math.sin(start + len); const x3 = cx + r2 * Math.cos(start + len), y3 = cy + r2 * Math.sin(start + len); const x4 = cx + r2 * Math.cos(start), y4 = cy + r2 * Math.sin(start); const large = len > Math.PI ? 1 : 0; return `M${x1},${y1} A${r},${r} 0 ${large} 1 ${x2},${y2} L${x3},${y3} A${r2},${r2} 0 ${large} 0 ${x4},${y4} Z`; }); return (
{paths.map((d, i) => )}
{segments.map((s, i) => (
{s.label} {s.v}%
))}
); } window.DashboardScreen = DashboardScreen;