feat: per-business-unit time multiplier (no data changes)
Adds a "Структурных подразделений (СП)" input. The xlsx source is not
modified; instead, at compute time functions whose name mentions СПП/ССП,
подразделения, руководители, комиссия, or линейн get their minutes
multiplied by the СП count. The table tags those rows with 🏢×N and shows
the base × multiplier breakdown.
This propagates through every KPI, chart, simulator, and FTE scenario
automatically because the multiplication happens in filtered() before
metrics().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
218c93df25
commit
3c937e091c
38
app.js
38
app.js
@ -15,6 +15,11 @@ const WORDS = {
|
||||
'девять':9,'девяти':9,'десять':10,'десяти':10,
|
||||
};
|
||||
|
||||
// Функции, время которых масштабируется по числу структурных подразделений (СП).
|
||||
// Признак — упоминание СПП/ССП, подразделений, руководителей в названии.
|
||||
const PER_UNIT_RE = /сп[пс]|подразделен|руководител|комисси|линейн/i;
|
||||
function isPerUnit(name) { return PER_UNIT_RE.test(String(name || '')); }
|
||||
|
||||
// Авто-классификация по этапам процесса (ключевые слова в названии функции)
|
||||
const STAGE_RULES = [
|
||||
['Привлечение', /публикац|hh\.kz|samruk|qsamruk|qazyna|поиск|базы|базы\s*холод|холодн|источник|ln\b/i],
|
||||
@ -38,6 +43,7 @@ const state = {
|
||||
search: '',
|
||||
vacancies: 15,
|
||||
salary: 500000,
|
||||
units: 1, // число структурных подразделений (СП)
|
||||
sort: { col: 'num', dir: 1 },
|
||||
sim: {
|
||||
enabled: false,
|
||||
@ -59,6 +65,7 @@ addEventListener('DOMContentLoaded', () => {
|
||||
$('theme-toggle').onclick = toggleTheme;
|
||||
$('inp-vacancies').oninput = e => { state.vacancies = +e.target.value || 0; state.sim.vacancies = state.vacancies; renderSimControls(); render(); };
|
||||
$('inp-salary').oninput = e => { state.salary = +e.target.value || 0; render(); };
|
||||
$('inp-units').oninput = e => { state.units = Math.max(1, +e.target.value || 1); render(); };
|
||||
initTableSort();
|
||||
initSimulator();
|
||||
autoLoad();
|
||||
@ -529,12 +536,17 @@ function renderTable(items, bottleneckIds = new Set()) {
|
||||
});
|
||||
document.querySelector('#table tbody').innerHTML = sorted.map(i => {
|
||||
const hot = bottleneckIds.has(i.num);
|
||||
const u = i.perUnit && i.unitsApplied > 1;
|
||||
const tag = (hot ? '🔥 ' : '') + (i.perUnit ? `🏢×${i.unitsApplied} ` : '');
|
||||
const minCell = i.minutes != null
|
||||
? (u ? `${Math.round(i.minutes)} <span class="mut">(${Math.round(i.baseMinutes)}×${i.unitsApplied})</span>` : Math.round(i.minutes))
|
||||
: '—';
|
||||
return `
|
||||
<tr class="${hot ? 'bottleneck' : ''}">
|
||||
<tr class="${hot ? 'bottleneck' : ''} ${i.perUnit ? 'per-unit' : ''}">
|
||||
<td>${i.num}</td>
|
||||
<td>${hot ? '🔥 ' : ''}${esc(i.name)}</td>
|
||||
<td>${tag}${esc(i.name)}</td>
|
||||
<td>${esc(String(i.rawTime ?? '—'))}</td>
|
||||
<td>${i.minutes != null ? Math.round(i.minutes) : '—'}</td>
|
||||
<td>${minCell}</td>
|
||||
<td>${esc(i.stage)}</td>
|
||||
<td>${esc(trunc(i.group, 60))}</td>
|
||||
</tr>`;
|
||||
@ -553,10 +565,22 @@ function sortVal(it, col) {
|
||||
|
||||
// === Helpers ===
|
||||
function filtered() {
|
||||
return state.items.filter(i =>
|
||||
(!state.groupFilter.length || state.groupFilter.includes(i.group)) &&
|
||||
(!state.search || i.name.toLowerCase().includes(state.search))
|
||||
);
|
||||
return state.items
|
||||
.filter(i =>
|
||||
(!state.groupFilter.length || state.groupFilter.includes(i.group)) &&
|
||||
(!state.search || i.name.toLowerCase().includes(state.search))
|
||||
)
|
||||
.map(i => {
|
||||
const perUnit = isPerUnit(i.name);
|
||||
const k = perUnit ? state.units : 1;
|
||||
return {
|
||||
...i,
|
||||
perUnit,
|
||||
unitsApplied: state.units,
|
||||
minutes: i.minutes == null ? null : i.minutes * k,
|
||||
baseMinutes: i.minutes,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function populateGroups() {
|
||||
|
||||
@ -35,6 +35,10 @@
|
||||
<div class="inputs">
|
||||
<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 title="Время функций, помеченных 🏢, умножается на это число">
|
||||
Структурных подразделений (СП)
|
||||
<input type="number" id="inp-units" value="1" min="1" step="1">
|
||||
</label>
|
||||
</div>
|
||||
<div id="strategic-kpi" class="kpi-grid"></div>
|
||||
</section>
|
||||
@ -142,6 +146,6 @@
|
||||
|
||||
<p id="placeholder">Загрузка данных… Если дашборд не появился — нажмите «📁 Загрузить .xlsx».</p>
|
||||
|
||||
<script src="./app.js?v=6"></script>
|
||||
<script src="./app.js?v=7"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -54,6 +54,8 @@ h2 .hint { display: inline; margin-left: .5rem; font-size: .75rem; }
|
||||
|
||||
tr.bottleneck td { background: rgba(248, 113, 113, 0.12); }
|
||||
tr.bottleneck td:nth-child(2) { font-weight: 600; }
|
||||
tr.per-unit td:nth-child(2) { color: var(--accent); }
|
||||
.mut { color: var(--muted); font-weight: normal; font-size: .8em; }
|
||||
|
||||
#table thead th { user-select: none; }
|
||||
#table thead th:hover { background: var(--accent); color: #fff; }
|
||||
|
||||
Reference in New Issue
Block a user