Маржа по направлениям
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}
);
})}
Ненормализованных
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 (
);
}
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 (
{segments.map((s, i) => (
{s.label}
{s.v}%
))}
);
}
window.DashboardScreen = DashboardScreen;