// 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,
});