// MDM SORP — shared primitives (FieldBadge, StatusChip, Icon, Toast, etc.) const { useState, useEffect, useRef, useMemo, useCallback, createContext, useContext } = React; /* ------------ I18N hook ------------ */ const I18nCtx = createContext({ lang: "ru", t: (k) => k }); const useI18n = () => useContext(I18nCtx); function I18nProvider({ children }) { const [lang, setLang] = useState(() => localStorage.getItem("sorp-lang") || "ru"); useEffect(() => { localStorage.setItem("sorp-lang", lang); document.documentElement.lang = lang; }, [lang]); const t = useCallback((key) => (window.I18N[lang] && window.I18N[lang][key]) || window.I18N.ru[key] || key, [lang]); return {children}; } /* ------------ Lucide-style inline SVG icons (1.5px stroke) ------------ */ const Icon = ({ name, size = 16, className = "" }) => { const paths = { home: <>, deals: <>, parties: <>, box: <>, grid: <>, docs: <>, queue: <>, merge: <>, upload: <>, shield: <>, bar: <>, cog: <>, plug: <>, book: <>, search: <>, bell: <>, plus: <>, minus: <>, check: <>, x: <>, arrow_right: <>, arrow_up_right: <>, chevron_right: <>, chevron_down: <>, filter: <>, sort: <>, download: <>, eye: <>, sparkle: <>, link: <>, undo: <>, history: <>, pencil: <>, copy: <>, more: <>, columns: <>, flag: <>, alert: <>, lightbulb: <>, list: <>, bookmark: <>, star: <>, info: <>, file: <>, image: <>, table: <>, kanban: <>, gallery: <>, calendar: <>, timeline: <>, group: <>, palette: <>, rows: <>, }; const p = paths[name] || paths.info; return ( ); }; /* ------------ FieldBadge ------------ */ const FieldBadge = ({ src = "ai", conf = 0.9, raw, when, applied }) => { const meta = window.SRC_META[src] || window.SRC_META.manual; const tone = conf < 0.5 ? "bad" : conf < 0.8 ? "warn" : "ok"; const pct = Math.round(conf * 100); return ( {meta.label} {pct}
Источник{meta.label}
Уверенность{pct}%
{raw !== undefined &&
Raw{raw || "—"}
} {when &&
Когда{when}
} {applied &&
Применено{applied}
}
); }; /* ------------ Chip / Status ------------ */ const Chip = ({ tone = "default", children, dot = false }) => ( {dot && } {children} ); const RoleChip = ({ role }) => { const map = { customer: { tone: "danger", label: "Заказчик", label_en: "Customer" }, supplier: { tone: "info", label: "Поставщик", label_en: "Supplier" }, end_user: { tone: "warning", label: "Конечник", label_en: "End user" }, vehicle: { tone: "ink", label: "Заход", label_en: "Vehicle" }, beneficiary:{ tone: "outline", label: "Бенефициар", label_en: "Beneficiary" }, contact: { tone: "outline", label: "Контакт", label_en: "Contact" }, employee: { tone: "outline", label: "Сотрудник", label_en: "Employee" }, }; const { lang } = useI18n(); const m = map[role] || { tone: "outline", label: role, label_en: role }; return {lang === "en" ? m.label_en : m.label}; }; const SanctionsChip = ({ status }) => { if (status === "flag") return Санкции; if (status === "review") return На проверке; if (status === "match") return Совпадение; if (status === "clean") return Чисто; return ; }; /* ------------ Avatar ------------ */ const Avatar = ({ name = "", size = 26, color }) => { const initials = name.split(/\s+/).map(s => s[0]).filter(Boolean).slice(0, 2).join("").toUpperCase(); const palette = ["#D63D4A", "#2C5C8C", "#2F7D5B", "#C77A1F", "#6E45E2", "#3A4049"]; const i = (name.charCodeAt(0) || 0) % palette.length; return ( {initials || "?"} ); }; /* ------------ Flag (country) ------------ */ const Flag = ({ code }) => { const c = (code || "").toLowerCase(); if (!["ru", "en", "cn", "ae", "de", "tr"].includes(c)) return ; const map = { tr: "linear-gradient(to bottom, #E30A17 50%, #fff 50%)" }; return ; }; /* ------------ Toast (with Undo) ------------ */ const ToastCtx = createContext(() => {}); const useToast = () => useContext(ToastCtx); function ToastHost({ children }) { const [list, setList] = useState([]); const push = useCallback((msg, opts = {}) => { const id = Math.random().toString(36).slice(2); setList(l => [...l, { id, msg, ...opts }]); setTimeout(() => setList(l => l.filter(t => t.id !== id)), opts.duration || 4000); }, []); return ( {children}
{list.map(t => (
{t.msg} {t.onUndo && }
))}
); } /* ------------ Drawer ------------ */ const Drawer = ({ open, onClose, children, width = 540 }) => { if (!open) return null; return ( <>
{children}
); }; /* ------------ Command palette ------------ */ function CommandPalette({ open, onClose, navigate }) { const { t } = useI18n(); const [q, setQ] = useState(""); const [active, setActive] = useState(0); useEffect(() => { setQ(""); setActive(0); }, [open]); const all = useMemo(() => { const deals = window.DATA.deals.map(d => ({ section: "cp_entities", icon: "deals", label: d.id + " · " + d.title, sub: d.stage, action: () => navigate("deal", { id: d.id }) })); const parties = window.DATA.parties.slice(0, 8).map(p => ({ section: "cp_entities", icon: "parties", label: p.short + " — " + p.full, sub: p.country, action: () => navigate("party", { id: p.id }) })); const actions = [ { section: "cp_actions", icon: "plus", label: t("cp_create_deal"), sub: "⏎", action: () => navigate("deal", { id: "SORP-3974" }) }, { section: "cp_actions", icon: "plus", label: t("cp_create_supplier"), sub: "⏎", action: () => navigate("parties") }, { section: "cp_actions", icon: "upload", label: t("cp_open_import"), sub: "⏎", action: () => navigate("import") }, { section: "cp_actions", icon: "shield", label: t("cp_run_sanctions"), sub: "⏎", action: () => navigate("sanctions") }, ]; return [...actions, ...deals, ...parties]; }, [navigate, t]); const filtered = useMemo(() => { const s = q.trim().toLowerCase(); if (!s) return all.slice(0, 10); return all.filter(x => x.label.toLowerCase().includes(s) || (x.sub || "").toLowerCase().includes(s)).slice(0, 10); }, [q, all]); const onKey = (e) => { if (e.key === "Escape") onClose(); else if (e.key === "ArrowDown") { e.preventDefault(); setActive(a => Math.min(filtered.length - 1, a + 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setActive(a => Math.max(0, a - 1)); } else if (e.key === "Enter") { e.preventDefault(); const r = filtered[active]; if (r) { r.action(); onClose(); } } }; if (!open) return null; let lastSection = null; return (
e.stopPropagation()}> { setQ(e.target.value); setActive(0); }} onKeyDown={onKey} placeholder={t("search")} />
{filtered.map((r, i) => { const showSection = r.section !== lastSection; lastSection = r.section; return ( {showSection &&
{t(r.section)}
}
setActive(i)} onClick={() => { r.action(); onClose(); }}> {r.label} {r.sub}
); })} {filtered.length === 0 &&
Нет совпадений
}
); } /* ------------ Money formatting ------------ */ const fmtMoney = (n, ccy = "USD") => { if (n == null) return "—"; const sym = { USD: "$", EUR: "€", AED: "AED ", RUB: "₽", CNY: "¥" }[ccy] || (ccy + " "); const num = n.toLocaleString("ru-RU", { maximumFractionDigits: 0 }); return ccy === "RUB" ? num + " ₽" : sym + num; }; /* expose for other scripts */ Object.assign(window, { I18nProvider, useI18n, I18nCtx, Icon, FieldBadge, Chip, RoleChip, SanctionsChip, Avatar, Flag, ToastHost, useToast, Drawer, CommandPalette, fmtMoney, });