Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
129 lines (115 loc) · 8.03 KB
/
Copy pathindex.html
File metadata and controls
129 lines (115 loc) · 8.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ozon — Пульт</title>
<link rel="stylesheet" href="assets/dash.css">
</head>
<body>
<div style="position:sticky;top:0;z-index:9999;background:#c0392b;color:#fff;font-family:sans-serif;font-weight:bold;text-align:center;padding:8px 12px;font-size:14px;letter-spacing:.3px;">ТЕСТОВАЯ ВЕРСИЯ · Задание №1 · f7f5a5d</div>
<div class="wrap">
<div id="navmount"></div>
<h1>Пульт</h1>
<p class="sub" id="subline">Загрузка…</p>
<div class="topbar" id="topbar"></div>
<div id="content">
<div class="loading">Загрузка данных…</div>
</div>
<div class="foot">
Данные — <code>data/dashboard.json</code>, собирается скриптом <code>build_all.py</code> из ozon-data.
Формулы прибыли/НДС/кредитов — см. <a href="methodology.html">Методология</a>.
</div>
</div>
<script src="assets/dash.js"></script>
<script>
Dash.loadAll(['data/dashboard.json']).then(function(res){
const r = res[0];
if (!r.ok){ Dash.showFatalError('content', ['data/dashboard.json ('+r.error+')']); return; }
const d = r.data;
const h = d.header || {};
document.getElementById('subline').textContent = 'Обновлено ' + (d.generated || '—');
Dash.setCalculatedAt(d.calculated_at);
const tb = document.getElementById('topbar');
const curW = h.current_week;
tb.innerHTML =
'<span class="pill">Последний закрытый день: '+Dash.fmtDate(h.last_closed_day)+'</span>'+
(h.last_full_week ? '<span class="pill">Последняя полная неделя: '+Dash.fmtDate(h.last_full_week.beg)+'—'+Dash.fmtDate(h.last_full_week.end)+'</span>' : '')+
(curW ? '<span class="pill">Текущая неделя: '+Dash.fmtDate(curW.beg)+'—'+Dash.fmtDate(curW.end)+' · '+(curW.closed?'закрыта':'не завершена')+'</span>' : '');
const k = d.kpi;
const kpiHtml =
'<div class="kpi-grid">'+
kpiCard('Реализовано', Dash.fmtRub(k.realized.value), k.realized.period, k.realized.delta, null, k.realized.meta)+
kpiCard('Операционная прибыль Ozon', Dash.fmtRub(k.profit_before.value),
k.profit_before.pct_of_realized!=null ? Dash.fmtPct(k.profit_before.pct_of_realized)+' от реализованного' : null,
k.profit_before.delta, null, k.profit_before.meta)+
kpiCard('Денежный результат после кредитов и расчётного НДС', Dash.fmtRub(k.net.value), null, k.net.delta, null, k.net.meta)+
kpiCard('Ozon должен нам', Dash.fmtRub(k.ozon_balance.value), k.ozon_balance.payout_note, null, null, k.ozon_balance.meta, 'Баланс')+
kpiCard('Потенциальные потери, ₽/30 дней', Dash.fmtRub(k.risk_30d.value), k.risk_30d.note, null, null, k.risk_30d.meta)+
'</div>';
const probs = d.top_problems.map(function(p){
return '<div class="problem"><div class="txt">'+Dash.reliabilityTag(p.reliability)+' <b>'+esc(p.type)+'</b> · '+
esc(p.name||p.object)+'<br><span class="dim">'+esc(p.problem)+' → '+esc(p.action)+'</span></div>'+
'<div class="eff">'+(p.effect_32d!=null ? Dash.fmtRub(p.effect_32d) : '—')+'</div>'+
'<a class="go" href="actions.html?type='+encodeURIComponent(p.type)+'">В «Действия» →</a></div>';
}).join('') || '<p class="dim">Нет активных проблем с рассчитанным эффектом.</p>';
const chart = renderWeeksChart(d.weeks_chart || []);
const cs = d.cost_structure;
const csHtml = cs ? [
['Реклама', cs.ads], ['Прочее Ozon', cs.other], ['Закупка', cs.zakup],
['Расчётный НДС', cs.vat], ['Кредиты', cs.loan],
].map(function(x){ return '<div class="rowcard"><div class="grid"><span>'+x[0]+'</span><b>'+Dash.fmtRub(x[1])+'</b></div></div>'; }).join('')
: '<p class="dim">Нет данных за последнюю полную неделю.</p>';
const inv = d.inventory_brief || {};
const invHtml =
'<div class="kpi-grid">'+
kpiCard('Запас в закупочных ценах', Dash.fmtRub(inv.cost_value), inv.n_positions+' позиций', null, 'inventory.html', inv.stock_meta, 'Остатки')+
kpiCard('Замороженный товар', Dash.fmtRub(inv.frozen_cost_value), inv.frozen_n+' SKU без движения '+inv.frozen_threshold_days+'+ дней', null, 'inventory.html', inv.frozen_meta, 'Остатки')+
kpiCard('Продававшиеся SKU без остатка', inv.zero_recent_sales_n, 'кандидаты на пополнение', null, 'inventory.html', inv.zero_recent_sales_meta)+
kpiCard('Товар в транзите', inv.in_transit_n+' SKU', null, null, 'inventory.html', inv.in_transit_meta, 'Состояние')+
'</div>';
document.getElementById('content').innerHTML =
kpiHtml +
'<h2>Требует решения</h2><div class="problem-list">'+probs+'</div>'+
'<h2>Реализовано / прибыль / результат по неделям</h2>'+chart+
'<h2>Структура расходов — последняя полная неделя</h2><div class="rowcards" style="display:flex">'+csHtml+'</div>'+
'<h2>Товар и склад — кратко</h2>'+invHtml+
'<h2>Выполнение задач</h2><p>Открыто: <b>'+d.actions_open_n+'</b> · выполнено: <b>'+d.actions_done_n+'</b> · '+
'<a href="actions.html">все действия →</a></p>';
}).catch(function(e){ Dash.showFatalError('content', ['dashboard.json ('+e+')']); });
function kpiCard(label, val, sub, delta, moreHref, meta, metaLabel){
return '<div class="kpi"><div class="lbl">'+esc(label)+'</div><div class="val">'+val+'</div>'+
(sub ? '<div class="period">'+esc(String(sub))+'</div>' : '')+
(delta ? '<div class="delta '+Dash.signClass(delta.abs||0)+'">'+Dash.deltaHtml(delta)+'</div>' : '')+
(meta ? Dash.renderMeta(meta, metaLabel) : '')+
(moreHref ? '<a class="more" href="'+moreHref+'">подробнее →</a>' : '')+
'</div>';
}
function renderWeeksChart(weeks){
if (!weeks.length) return '<p class="dim">Недостаточно закрытых недель для графика.</p>';
const W=680,H=200,pad=30;
const vals = weeks.map(function(w){return w.sales||0}).concat(weeks.map(function(w){return w.profit_before||0}));
const max = Math.max.apply(null, vals.concat([0])), min = Math.min.apply(null, vals.concat([0]));
const bw = (W-2*pad)/weeks.length;
function y(v){ return H-pad - (v-min)/(max-min||1)*(H-2*pad); }
function pts(key){
return weeks.map(function(w,i){ return (pad+bw*i+bw/2)+','+y(w[key]||0); }).join(' ');
}
const bars = weeks.map(function(w,i){
const bh = (H-pad) - y(w.sales||0);
return '<rect x="'+(pad+bw*i+bw*0.15)+'" y="'+y(w.sales||0)+'" width="'+(bw*0.7)+'" height="'+Math.max(bh,0)+
'" fill="var(--line)"><title>'+w.beg+' — '+w.sales+' ₽</title></rect>';
}).join('');
return '<div class="card" style="padding:10px 6px"><svg viewBox="0 0 '+W+' '+H+'" style="width:100%;height:auto" xmlns="http://www.w3.org/2000/svg">'+
bars+
'<polyline points="'+pts('profit_before')+'" fill="none" stroke="var(--accent)" stroke-width="2"/>'+
'<polyline points="'+pts('net')+'" fill="none" stroke="var(--neg)" stroke-width="2" stroke-dasharray="4 3"/>'+
'<line x1="'+pad+'" y1="'+y(0)+'" x2="'+(W-pad)+'" y2="'+y(0)+'" stroke="var(--muted)" stroke-width="1"/>'+
'</svg><div style="display:flex;gap:14px;font-size:11px;color:var(--ink2);padding:4px 10px">'+
'<span>▭ Реализовано</span><span style="color:var(--accent)">— Операционная прибыль</span>'+
'<span style="color:var(--neg)">┄ Денежный результат</span></div></div>';
}
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"'}[c]; }); }
</script>
</body>
</html>