feat: emoji, 2-recruiter split, weekly chart, FTE multi-line scenario
- добавлены эмодзи во все заголовки и кнопки - новый раздел «Распределение на 2 рекрутеров» с KPI-карточками - понедельный план: сгруппированный бар-чарт (Рекрутер 1 / Рекрутер 2) - сценарий вакансий переделан в мульти-линейный (1/2/3 рекрутера), цветовые зоны, ромбы ёмкости, аннотации FTE на текущем плане Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1244c52e41
commit
f03de26637
164
app.js
164
app.js
@ -294,12 +294,21 @@ function metrics(items) {
|
|||||||
// Bottleneck top-3 num set
|
// Bottleneck top-3 num set
|
||||||
const bottleneckIds = new Set(top10.slice(0, 3).map(i => i.num));
|
const bottleneckIds = new Set(top10.slice(0, 3).map(i => i.num));
|
||||||
|
|
||||||
|
// Распределение на 2 рекрутеров
|
||||||
|
const r2DaysEach = monthlyDays / 2;
|
||||||
|
const r2WeeksEach = r2DaysEach / DAYS_WEEK;
|
||||||
|
const r2FteEach = fte / 2;
|
||||||
|
const r2OverloadEach = r2DaysEach - DAYS_MONTH;
|
||||||
|
const r2DaysPerVac = daysPerCycle / 2; // дней на 1 вакансию при работе двоих
|
||||||
|
const r2DaysPerWeek = r2DaysEach / (DAYS_MONTH / DAYS_WEEK); // дней/нед на 1 рекрутера
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total, hours, count: items.length, avg: valid.length ? total / valid.length : 0,
|
total, hours, count: items.length, avg: valid.length ? total / valid.length : 0,
|
||||||
longest, top10, byGroup,
|
longest, top10, byGroup,
|
||||||
fte, capacity, bottleneck, cost,
|
fte, capacity, bottleneck, cost,
|
||||||
daysPerCycle, weeksPerCycle, monthlyDays, monthlyWeeks, monthOverload, recruitersNeeded,
|
daysPerCycle, weeksPerCycle, monthlyDays, monthlyWeeks, monthOverload, recruitersNeeded,
|
||||||
pareto, pareto80, histogram, duration, durationMin, byStage, bottleneckIds,
|
pareto, pareto80, histogram, duration, durationMin, byStage, bottleneckIds,
|
||||||
|
r2DaysEach, r2WeeksEach, r2FteEach, r2OverloadEach, r2DaysPerVac, r2DaysPerWeek,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,6 +349,9 @@ function render() {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
renderTwoRecruiterKPI(m);
|
||||||
|
chartWeeklyDistribution(m.daysPerCycle, state.vacancies);
|
||||||
|
|
||||||
chartTop10(m.top10);
|
chartTop10(m.top10);
|
||||||
chartPareto(m.pareto, m.pareto80);
|
chartPareto(m.pareto, m.pareto80);
|
||||||
chartHistogram(m.histogram);
|
chartHistogram(m.histogram);
|
||||||
@ -538,22 +550,146 @@ function chartStages(byStage) {
|
|||||||
|
|
||||||
function chartFteScenario(daysPerCycle) {
|
function chartFteScenario(daysPerCycle) {
|
||||||
if (!daysPerCycle) return Plotly.purge('chart-fte-scenario');
|
if (!daysPerCycle) return Plotly.purge('chart-fte-scenario');
|
||||||
const xs = Array.from({ length: 31 }, (_, i) => i); // 0..30 вакансий
|
|
||||||
const ftePct = xs.map(v => v * daysPerCycle / DAYS_MONTH * 100);
|
const MAX_VAC = 20;
|
||||||
const colors = ftePct.map(p => p <= 100 ? '#34d399' : p <= 150 ? '#fbbf24' : '#f87171');
|
const xs = Array.from({ length: MAX_VAC + 1 }, (_, i) => i);
|
||||||
Plotly.newPlot('chart-fte-scenario', [{
|
const fteFor = (v, r) => r > 0 ? v * daysPerCycle / DAYS_MONTH / r * 100 : 0;
|
||||||
type: 'bar', x: xs, y: ftePct, marker: { color: colors },
|
|
||||||
text: xs.map(v => v === state.vacancies ? '← план' : ''),
|
const TEAMS = [1, 2, 3];
|
||||||
textposition: 'outside',
|
const COLORS = ['#f87171', '#fbbf24', '#34d399'];
|
||||||
hovertemplate: '<b>%{x} вак/мес</b><br>FTE: %{y:.0f}%<extra></extra>',
|
const NAMES = ['1 рекрутер', '2 рекрутера', '3 рекрутера'];
|
||||||
}], layout({
|
const DASHES = ['solid', 'dash', 'dot'];
|
||||||
height: 320, margin: { l: 50, r: 20, t: 20, b: 50 },
|
|
||||||
xaxis: { title: 'Вакансий в месяц' },
|
// Cap Y so chart stays readable
|
||||||
yaxis: { title: 'Загрузка FTE, %', ticksuffix: '%' },
|
const rawMax = fteFor(MAX_VAC, 1);
|
||||||
|
const capY = Math.min(rawMax, 600);
|
||||||
|
|
||||||
|
const traces = TEAMS.map((r, i) => ({
|
||||||
|
type: 'scatter', mode: 'lines+markers',
|
||||||
|
name: NAMES[i],
|
||||||
|
x: xs,
|
||||||
|
y: xs.map(v => +Math.min(fteFor(v, r), capY + 10).toFixed(1)),
|
||||||
|
line: { color: COLORS[i], width: 2.5, dash: DASHES[i] },
|
||||||
|
marker: { size: 5 },
|
||||||
|
hovertemplate: `<b>${NAMES[i]}</b><br>%{x} вак → %{y:.0f}% FTE<extra></extra>`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Capacity — max vacancies at 100% FTE per team size
|
||||||
|
const capTraces = TEAMS.map((r, i) => {
|
||||||
|
const capVac = DAYS_MONTH * r / daysPerCycle;
|
||||||
|
return {
|
||||||
|
type: 'scatter', mode: 'markers+text',
|
||||||
|
name: `Макс. ${NAMES[i]}`,
|
||||||
|
x: [+capVac.toFixed(1)], y: [100],
|
||||||
|
marker: { symbol: 'diamond', size: 10, color: COLORS[i], line: { color: '#fff', width: 1 } },
|
||||||
|
text: [`${capVac.toFixed(1)} вак`],
|
||||||
|
textposition: i === 0 ? 'top right' : i === 1 ? 'top center' : 'top left',
|
||||||
|
textfont: { color: COLORS[i], size: 10 },
|
||||||
|
showlegend: false,
|
||||||
|
hovertemplate: `<b>${NAMES[i]}</b><br>Макс при норме: %{x:.1f} вак/мес<extra></extra>`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Plan FTE annotations for each team size
|
||||||
|
const planAnnot = TEAMS.map((r, i) => ({
|
||||||
|
x: state.vacancies,
|
||||||
|
y: Math.min(fteFor(state.vacancies, r), capY + 10),
|
||||||
|
text: `${Math.round(fteFor(state.vacancies, r))}%`,
|
||||||
|
showarrow: true, arrowhead: 2,
|
||||||
|
ax: 32 - i * 20, ay: -22 - i * 14,
|
||||||
|
font: { color: COLORS[i], size: 11, weight: 700 },
|
||||||
|
arrowcolor: COLORS[i],
|
||||||
|
}));
|
||||||
|
|
||||||
|
Plotly.newPlot('chart-fte-scenario', [...traces, ...capTraces], layout({
|
||||||
|
height: 400,
|
||||||
|
margin: { l: 58, r: 20, t: 20, b: 70 },
|
||||||
|
xaxis: { title: 'Вакансий в месяц', range: [0, MAX_VAC], dtick: 2 },
|
||||||
|
yaxis: { title: 'Загрузка FTE, %', ticksuffix: '%', range: [0, capY + 30] },
|
||||||
shapes: [
|
shapes: [
|
||||||
{ type: 'line', xref: 'x', yref: 'y', x0: -0.5, x1: 30.5, y0: 100, y1: 100, line: { color: '#f87171', width: 2, dash: 'dash' } },
|
// цветовые зоны фона
|
||||||
|
{ type: 'rect', xref: 'paper', yref: 'y', x0: 0, x1: 1, y0: 0, y1: 100, fillcolor: 'rgba(52,211,153,.07)', line: { width: 0 } },
|
||||||
|
{ type: 'rect', xref: 'paper', yref: 'y', x0: 0, x1: 1, y0: 100, y1: 150, fillcolor: 'rgba(251,191,36,.07)', line: { width: 0 } },
|
||||||
|
{ type: 'rect', xref: 'paper', yref: 'y', x0: 0, x1: 1, y0: 150, y1: capY + 30, fillcolor: 'rgba(248,113,113,.07)', line: { width: 0 } },
|
||||||
|
// норма 100%
|
||||||
|
{ type: 'line', xref: 'paper', yref: 'y', x0: 0, x1: 1, y0: 100, y1: 100, line: { color: '#94a3b8', width: 1.5, dash: 'dash' } },
|
||||||
|
// 150%
|
||||||
|
{ type: 'line', xref: 'paper', yref: 'y', x0: 0, x1: 1, y0: 150, y1: 150, line: { color: '#fbbf24', width: 1, dash: 'dot' } },
|
||||||
|
// вертикаль — текущий план
|
||||||
|
{ type: 'line', xref: 'x', yref: 'paper', x0: state.vacancies, x1: state.vacancies, y0: 0, y1: 1, line: { color: '#a78bfa', width: 2, dash: 'dash' } },
|
||||||
],
|
],
|
||||||
annotations: [{ x: 30, y: 100, xanchor: 'right', yanchor: 'bottom', text: 'Норма 100% FTE', showarrow: false, font: { color: '#f87171' } }],
|
annotations: [
|
||||||
|
{ xref: 'paper', yref: 'y', x: 0.01, y: 100, xanchor: 'left', yanchor: 'bottom', text: '100% норма', showarrow: false, font: { color: '#94a3b8', size: 10 } },
|
||||||
|
{ xref: 'paper', yref: 'y', x: 0.01, y: 150, xanchor: 'left', yanchor: 'bottom', text: '150% перегруз', showarrow: false, font: { color: '#fbbf24', size: 10 } },
|
||||||
|
{ xref: 'x', yref: 'paper', x: state.vacancies, y: 1.02, xanchor: 'center', text: `план: ${state.vacancies} вак`, showarrow: false, font: { color: '#a78bfa', size: 10 } },
|
||||||
|
...planAnnot,
|
||||||
|
],
|
||||||
|
legend: { orientation: 'h', y: -0.22 },
|
||||||
|
}), cfg());
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTwoRecruiterKPI(m) {
|
||||||
|
const overEach = m.r2OverloadEach;
|
||||||
|
renderCards('two-recruiter-kpi', [
|
||||||
|
{ value: `${m.r2DaysEach.toFixed(1)} дн`, label: 'Дней/мес на 1 рекрутера', sub: `${state.vacancies} вак × ${m.r2DaysPerVac.toFixed(1)} дн` },
|
||||||
|
{ value: `${m.r2WeeksEach.toFixed(2)} нед`, label: 'Недель/мес на 1 рекрутера', sub: `норма — ${(DAYS_MONTH / DAYS_WEEK).toFixed(1)} нед` },
|
||||||
|
{ value: `${Math.round(m.r2FteEach)}%`, label: 'Загрузка FTE каждого', sub: 'при равномерном делении' },
|
||||||
|
{ value: `${m.r2DaysPerWeek.toFixed(1)} дн/нед`, label: 'Нагрузка на 1 рекрутера в неделю', sub: 'норма — 5 дн/нед' },
|
||||||
|
{
|
||||||
|
value: overEach >= 0 ? `+${overEach.toFixed(1)} дн` : `${overEach.toFixed(1)} дн`,
|
||||||
|
label: overEach > 0 ? '⚠ Перегруз при 2 рекрутерах' : '✓ Запас при 2 рекрутерах',
|
||||||
|
sub: overEach > 0
|
||||||
|
? `ещё нужно +${(overEach / DAYS_MONTH).toFixed(2)} FTE`
|
||||||
|
: `свободно ${Math.abs(overEach).toFixed(1)} дн`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function chartWeeklyDistribution(daysPerCycle, vacancies) {
|
||||||
|
const el = 'chart-weekly-split';
|
||||||
|
if (!daysPerCycle || !vacancies) return Plotly.purge(el);
|
||||||
|
|
||||||
|
const WEEKS = 4;
|
||||||
|
const base = Math.floor(vacancies / WEEKS);
|
||||||
|
const extra = vacancies % WEEKS;
|
||||||
|
const vacDist = Array.from({ length: WEEKS }, (_, i) => base + (i < extra ? 1 : 0));
|
||||||
|
const weeks = ['Неделя 1', 'Неделя 2', 'Неделя 3', 'Неделя 4'];
|
||||||
|
|
||||||
|
const r1 = vacDist.map(v => +(v * daysPerCycle / 2).toFixed(2));
|
||||||
|
const r2 = vacDist.map(v => +(v * daysPerCycle / 2).toFixed(2));
|
||||||
|
|
||||||
|
Plotly.newPlot(el, [
|
||||||
|
{
|
||||||
|
type: 'bar', name: '👤 Рекрутер 1',
|
||||||
|
x: weeks, y: r1,
|
||||||
|
marker: { color: '#60a5fa' },
|
||||||
|
text: r1.map(d => `${d.toFixed(1)} дн`), textposition: 'outside',
|
||||||
|
hovertemplate: '<b>%{x}</b><br>Рекрутер 1: %{y:.1f} дн<extra></extra>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'bar', name: '👤 Рекрутер 2',
|
||||||
|
x: weeks, y: r2,
|
||||||
|
marker: { color: '#34d399' },
|
||||||
|
text: r2.map(d => `${d.toFixed(1)} дн`), textposition: 'outside',
|
||||||
|
hovertemplate: '<b>%{x}</b><br>Рекрутер 2: %{y:.1f} дн<extra></extra>',
|
||||||
|
},
|
||||||
|
], layout({
|
||||||
|
height: 340,
|
||||||
|
barmode: 'group',
|
||||||
|
margin: { l: 55, r: 20, t: 30, b: 80 },
|
||||||
|
yaxis: { title: 'Рабочих дней' },
|
||||||
|
shapes: [{
|
||||||
|
type: 'line', xref: 'paper', yref: 'y',
|
||||||
|
x0: 0, x1: 1, y0: DAYS_WEEK, y1: DAYS_WEEK,
|
||||||
|
line: { color: '#f87171', width: 2, dash: 'dash' },
|
||||||
|
}],
|
||||||
|
annotations: [{
|
||||||
|
x: 1, y: DAYS_WEEK, xref: 'paper', yref: 'y',
|
||||||
|
xanchor: 'right', yanchor: 'bottom',
|
||||||
|
text: '5 дн — норма недели', showarrow: false,
|
||||||
|
font: { color: '#f87171', size: 11 },
|
||||||
|
}],
|
||||||
|
legend: { orientation: 'h', y: -0.22 },
|
||||||
}), cfg());
|
}), cfg());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
44
index.html
44
index.html
@ -11,7 +11,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<img src="./assets/logo.svg" alt="telecom.quest" class="logo">
|
<img src="./assets/logo.svg" alt="telecom.quest" class="logo">
|
||||||
<h1>Дашборд функций рекрутинга</h1>
|
<h1>🚀 Дашборд функций рекрутинга</h1>
|
||||||
<button id="theme-toggle" aria-label="Тема">🌙</button>
|
<button id="theme-toggle" aria-label="Тема">🌙</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="hero-text">
|
<div class="hero-text">
|
||||||
<h2>Аналитика рекрутинга</h2>
|
<h2>🔬 Аналитика рекрутинга</h2>
|
||||||
<p>Время функций → KPI, нагрузка, узкие места, симулятор «что если». Без сервера: данные парсятся прямо в браузере.</p>
|
<p>Время функций → KPI, нагрузка, узкие места, симулятор «что если». Без сервера: данные парсятся прямо в браузере.</p>
|
||||||
</div>
|
</div>
|
||||||
<img src="./assets/hero.svg" alt="" class="hero-img" loading="lazy">
|
<img src="./assets/hero.svg" alt="" class="hero-img" loading="lazy">
|
||||||
@ -34,12 +34,12 @@
|
|||||||
|
|
||||||
<main id="dashboard" hidden>
|
<main id="dashboard" hidden>
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2>Базовые показатели</h2>
|
<h2>🔢 Базовые показатели</h2>
|
||||||
<div id="kpi" class="kpi-grid"></div>
|
<div id="kpi" class="kpi-grid"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2>📊 Стратегические KPI</h2>
|
<h2>🏹 Стратегические KPI</h2>
|
||||||
<div class="inputs">
|
<div class="inputs">
|
||||||
<label>Вакансий/мес <input type="number" id="inp-vacancies" value="15" min="0"></label>
|
<label>Вакансий/мес <input type="number" id="inp-vacancies" value="15" min="0"></label>
|
||||||
<label>Ставка ₸/мес <input type="number" id="inp-salary" value="500000" min="0" step="10000"></label>
|
<label>Ставка ₸/мес <input type="number" id="inp-salary" value="500000" min="0" step="10000"></label>
|
||||||
@ -54,7 +54,7 @@
|
|||||||
<section class="block with-mascot">
|
<section class="block with-mascot">
|
||||||
<img src="./assets/recruiter.svg" alt="" class="mascot" loading="lazy">
|
<img src="./assets/recruiter.svg" alt="" class="mascot" loading="lazy">
|
||||||
<div class="block-body">
|
<div class="block-body">
|
||||||
<h2>👤 Нагрузка на сотрудника (дни и недели)</h2>
|
<h2>🧑💼 Нагрузка на сотрудника (дни и недели)</h2>
|
||||||
<div id="workload-kpi" class="kpi-grid"></div>
|
<div id="workload-kpi" class="kpi-grid"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -62,22 +62,30 @@
|
|||||||
<section class="block with-mascot mascot-right">
|
<section class="block with-mascot mascot-right">
|
||||||
<img src="./assets/candidate.svg" alt="" class="mascot" loading="lazy">
|
<img src="./assets/candidate.svg" alt="" class="mascot" loading="lazy">
|
||||||
<div class="block-body">
|
<div class="block-body">
|
||||||
<h2>✏️ Ручные правки времени (без изменения xlsx)</h2>
|
<h2>🛠️ Ручные правки времени (без изменения xlsx)</h2>
|
||||||
<p class="hint">Заменить минуты конкретной функции по №. Влияет на все KPI и графики. Пусто = убрать правку.</p>
|
<p class="hint">Заменить минуты конкретной функции по №. Влияет на все KPI и графики. Пусто = убрать правку.</p>
|
||||||
<div class="ovr-form">
|
<div class="ovr-form">
|
||||||
<label>№ функции <input type="number" id="ovr-num" min="1" placeholder="например 39"></label>
|
<label>№ функции <input type="number" id="ovr-num" min="1" placeholder="например 39"></label>
|
||||||
<label>Время, мин <input type="number" id="ovr-min" min="0" placeholder="например 60"></label>
|
<label>Время, мин <input type="number" id="ovr-min" min="0" placeholder="например 60"></label>
|
||||||
<button type="button" id="ovr-apply">Применить</button>
|
<button type="button" id="ovr-apply">💾 Применить</button>
|
||||||
<button type="button" id="ovr-clear" class="secondary">Очистить все</button>
|
<button type="button" id="ovr-clear" class="secondary">🧹 Очистить все</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="ovr-list" class="ovr-list"></div>
|
<div id="ovr-list" class="ovr-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="block">
|
||||||
|
<h2>👥 Распределение на 2 рекрутеров</h2>
|
||||||
|
<p class="hint">Равномерное распределение текущей нагрузки без изменения данных xlsx.</p>
|
||||||
|
<div id="two-recruiter-kpi" class="kpi-grid"></div>
|
||||||
|
<h3 class="section-subhead">📅 Понедельный план месяца</h3>
|
||||||
|
<div id="chart-weekly-split"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="block with-mascot">
|
<section class="block with-mascot">
|
||||||
<img src="./assets/thinker.svg" alt="" class="mascot" loading="lazy">
|
<img src="./assets/thinker.svg" alt="" class="mascot" loading="lazy">
|
||||||
<div class="block-body">
|
<div class="block-body">
|
||||||
<h2>🧪 Симулятор «что если»</h2>
|
<h2>🎮 Симулятор «что если»</h2>
|
||||||
<p class="hint">Не меняет исходные данные. Покажет, как изменятся KPI при заданных условиях.</p>
|
<p class="hint">Не меняет исходные данные. Покажет, как изменятся KPI при заданных условиях.</p>
|
||||||
<label class="sim-toggle"><input type="checkbox" id="sim-toggle"> Включить симулятор</label>
|
<label class="sim-toggle"><input type="checkbox" id="sim-toggle"> Включить симулятор</label>
|
||||||
|
|
||||||
@ -113,48 +121,48 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2>📈 Сценарий: FTE % vs число вакансий</h2>
|
<h2>📡 Сценарий: FTE % vs число вакансий</h2>
|
||||||
<p class="hint">Зелёный — норма (≤100%), жёлтый — перегруз ≤150%, красный — критично. Пунктир — 100% FTE.</p>
|
<p class="hint">3 линии — 1, 2 и 3 рекрутера. Ромбы ◆ — максимум вакансий при 100% норме. Фиолетовый пунктир — текущий план. Зоны: зелёная ≤100%, жёлтая ≤150%, красная — критично.</p>
|
||||||
<div id="chart-fte-scenario"></div>
|
<div id="chart-fte-scenario"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2>Топ-10 самых долгих функций</h2>
|
<h2>⏳ Топ-10 самых долгих функций</h2>
|
||||||
<div id="chart-top10"></div>
|
<div id="chart-top10"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2>📉 Pareto: накопительный % времени</h2>
|
<h2>🎯 Pareto: накопительный % времени</h2>
|
||||||
<p class="hint">Какие функции дают 80% всей нагрузки. Жёлтая линия — порог 80%.</p>
|
<p class="hint">Какие функции дают 80% всей нагрузки. Жёлтая линия — порог 80%.</p>
|
||||||
<div id="chart-pareto"></div>
|
<div id="chart-pareto"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="charts-row">
|
<section class="charts-row">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h2>📊 Гистограмма длительности</h2>
|
<h2>📶 Гистограмма длительности</h2>
|
||||||
<div id="chart-histogram"></div>
|
<div id="chart-histogram"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h2>⏱ Категории функций</h2>
|
<h2>🗂️ Категории функций</h2>
|
||||||
<div id="chart-duration"></div>
|
<div id="chart-duration"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2>🧭 Авто-этапы (по ключевым словам в названии)</h2>
|
<h2>🔄 Авто-этапы (по ключевым словам в названии)</h2>
|
||||||
<p class="hint">Эвристика — не правит данные. Привлечение / Скрининг / Интервью / Оффер / Адаптация / Документы-СПП / Коммуникация.</p>
|
<p class="hint">Эвристика — не правит данные. Привлечение / Скрининг / Интервью / Оффер / Адаптация / Документы-СПП / Коммуникация.</p>
|
||||||
<div id="chart-stages"></div>
|
<div id="chart-stages"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2>🗺 Treemap: группа → функция (площадь = время)</h2>
|
<h2>🌳 Treemap: группа → функция (площадь = время)</h2>
|
||||||
<div id="chart-treemap"></div>
|
<div id="chart-treemap"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="block with-mascot mascot-right mascot-wide">
|
<section class="block with-mascot mascot-right mascot-wide">
|
||||||
<img src="./assets/team.svg" alt="" class="mascot" loading="lazy">
|
<img src="./assets/team.svg" alt="" class="mascot" loading="lazy">
|
||||||
<div class="block-body">
|
<div class="block-body">
|
||||||
<h2>Все функции <span class="hint">(клик по заголовку — сортировка)</span></h2>
|
<h2>📑 Все функции <span class="hint">(клик по заголовку — сортировка)</span></h2>
|
||||||
<table id="table">
|
<table id="table">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th data-col="num">№</th>
|
<th data-col="num">№</th>
|
||||||
|
|||||||
@ -125,6 +125,7 @@ main { padding: 1.5rem 2rem; display: flex; flex-direction: column; gap: 1.25rem
|
|||||||
#chart-histogram, #chart-duration { min-height: 300px; }
|
#chart-histogram, #chart-duration { min-height: 300px; }
|
||||||
|
|
||||||
.hint { color: var(--muted); font-size: .8rem; margin: -.5rem 0 1rem; font-weight: normal; }
|
.hint { color: var(--muted); font-size: .8rem; margin: -.5rem 0 1rem; font-weight: normal; }
|
||||||
|
.section-subhead { font-size: .95rem; font-weight: 600; margin: 1.5rem 0 .5rem; opacity: .85; letter-spacing: .01em; }
|
||||||
h2 .hint { display: inline; margin-left: .5rem; font-size: .75rem; }
|
h2 .hint { display: inline; margin-left: .5rem; font-size: .75rem; }
|
||||||
|
|
||||||
tr.bottleneck td { background: rgba(248, 113, 113, 0.12); }
|
tr.bottleneck td { background: rgba(248, 113, 113, 0.12); }
|
||||||
|
|||||||
Reference in New Issue
Block a user