Отдельная страница для разговора с клиентом: где ещё логичнее инжектор, а где на больших тиражах уже начинает выигрывать ТПА.
Не “ещё один калькулятор”, а понятная история про то, когда клиенту уже стоит идти в ТПА.
Бланки ниже нужны не как будущий ассортимент ТПА, а как понятные референсы для переговоров. Смотрим на форму, вес и тираж и на пальцах объясняем: здесь выгоднее инжектор, здесь переходная зона, а здесь новый клиентский молд на ТПА уже имеет смысл.
Кастомный молд под клиентаОператор полностью занят у станкаФОТ и косвенные падают на все часы партииОбрезание литника не считаем отдельно
Станок
${this.escape(this.MACHINE.model)}
17 тонн, компактный all-electric
Лимит по выстрелу
${this.MACHINE.shotWeightGrams} г
то есть легкие и тонкие детали выигрывают
Макс. молд
${this.escape(this.MACHINE.moldSize)}
большие монолитные штуки быстро упираются в размер и охлаждение
Старт интереса
30–50k
на простых плоских формах, если геометрия подходит
Как мы считаем ТПА сейчас
Консервативная модель
Здесь нет “мягкого наблюдения боком”. Мы специально считаем жестко и честно: человек сидит у станка всю партию, параллельно только подрезает литник, поэтому отдельную операцию обрезания не добавляем. Зато весь ФОТ в час и все косвенные в час ложатся на часы этой партии.
Что ложится в runtime
Пластик, ФОТ оператора за все часы партии, косвенные за эти же часы и запуск партии.
Что отдельно размазывается
Только стоимость клиентской формы: молд делится на тираж заказа, а не на гипотетический сток.
Почему это полезно
Получается строгий ориентир для продаж: если даже в такой модели ТПА уже дышит, значит кейс правда сильный.
Зачем бланки снизу
Они помогают объяснять клиенту не “наш ассортимент”, а саму логику: плоская тонкая геометрия любит ТПА, толстая и вставная нет.
Живой расчет
Считаем
Подставьте свой сценарий или ткните в конкретную бланковую форму ниже. Страница пересчитает и общую себестоимость, и “вердикт для клиента” на выбранном тираже.
Тиражная таблица
Один взгляд на “когда начинает дышать”
Берем текущий сценарий выше и размазываем форму по разным тиражам. Если форма на штуку больше самого runtime, клиенту проще объяснять инжектор. Когда форма садится ниже runtime, ТПА уже становится вкусным.
Тираж
Скорость
Часы
Runtime / шт
Форма / шт
Себест. / шт
Цена без НДС
Вердикт
Ограничения XPM-17
Физика важнее желания
Усилие смыкания${this.MACHINE.clampTons}T
Макс. вес выстрела${this.MACHINE.shotWeightGrams} г
Размер формы${this.escape(this.MACHINE.moldSize)}
Шнек / сопло${this.escape(this.MACHINE.screw)} / 3 мм
Ход раскрытия${this.escape(this.MACHINE.stroke)}
Выводлюбит тонкие, легкие и повторяемые детали
Что продает ТПА клиенту
Хорошо для ТПА
Плоские бланки, тэги, ключи, буквы, мелкие фигурки и другие легкие детали, где можно разложить 4–8 гнезд и крутить длинные тиражи.
Переходная зона
Средние по площади формы, сложный контур, ребристая объемная деталь типа дженги. Технически можно, но тираж и гнездность решают всё.
Где объяснять инжектор
Толстые монолитные формы, вещи со вставкой, NFC, карабины 6–8 мм, большие драконы, кроссовки и другие детали, где этот XPM-17 слишком слаб или теряет свой безлюдный смысл.
Как говорить про косвенные
Бланковые формы как референсы для клиентов
Ниже не “что мы обязаны делать на ТПА”, а удобный язык продаж. Берем знакомую форму и быстро объясняем клиенту, в каком режиме его тираж сейчас живет.
`;
},
bindEvents() {
this.root.addEventListener('click', (event) => {
const presetBtn = event.target.closest('[data-tpa-preset]');
if (presetBtn) {
this.applyPreset(presetBtn.dataset.tpaPreset);
return;
}
const actionBtn = event.target.closest('[data-tpa-action]');
if (actionBtn) {
const action = actionBtn.dataset.tpaAction;
if (action === 'open-molds') App.navigate('molds');
if (action === 'open-calculator') App.navigate('calculator');
return;
}
const blankBtn = event.target.closest('[data-tpa-fill-blank]');
if (blankBtn) {
this.selectBlank(blankBtn.dataset.tpaFillBlank);
}
});
this.root.addEventListener('input', (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const idMap = {
'tpa-quantity': 'quantity',
'tpa-weight': 'weight',
'tpa-cavities': 'cavities',
'tpa-openings': 'openingsPerHour',
'tpa-mold-cost': 'moldCost',
'tpa-setup-cost': 'setupCost',
'tpa-material-cost': 'materialCostPerKg',
'tpa-operator-rate': 'operatorRatePerHour',
'tpa-indirect-rate': 'indirectRatePerHour',
'tpa-waste-factor': 'wasteFactor',
'tpa-target-margin': 'targetMarginPct',
};
const key = idMap[target.id];
if (!key) return;
this.state[key] = this.readNumber(target.value, this.state[key]);
this.state.preset = 'custom';
this.refresh();
});
this.root.addEventListener('change', (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
if (target.id === 'tpa-geometry') {
this.state.geometry = target.value || 'flat';
this.state.preset = 'custom';
this.refresh();
return;
}
if (target.id === 'tpa-collection-filter') {
this.state.collectionFilter = target.value || 'all';
this.refreshBlanks();
return;
}
if (target.id === 'tpa-verdict-filter') {
this.state.verdictFilter = target.value || 'all';
this.refreshBlanks();
}
});
},
readNumber(value, fallback) {
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : fallback;
},
applyPreset(presetName) {
const preset = this.PRESETS[presetName];
if (!preset) return;
this.state = {
...this.state,
...preset.values,
preset: presetName,
selectedBlankId: null,
};
this.refresh();
},
selectBlank(blankId) {
const blank = this.blanks.find(item => String(item.id) === String(blankId));
if (!blank) return;
const assessment = this.assessBlank(blank);
this.state = {
...this.state,
preset: 'custom',
selectedBlankId: blank.id,
geometry: assessment.geometry,
weight: blank.tpaWeight || this.state.weight,
cavities: assessment.recommendedCavities,
openingsPerHour: assessment.openingsPerHour,
};
this.refresh();
},
refresh() {
if (!this.root) return;
this.syncInputs();
this.refreshStaticBlocks();
this.refreshResults();
this.refreshBlanks();
},
syncInputs() {
const state = this.state;
this.setInputValue('tpa-quantity', state.quantity);
this.setInputValue('tpa-weight', state.weight);
this.setInputValue('tpa-cavities', state.cavities);
this.setInputValue('tpa-openings', state.openingsPerHour);
this.setInputValue('tpa-mold-cost', state.moldCost);
this.setInputValue('tpa-setup-cost', state.setupCost);
this.setInputValue('tpa-material-cost', state.materialCostPerKg);
this.setInputValue('tpa-operator-rate', state.operatorRatePerHour);
this.setInputValue('tpa-indirect-rate', state.indirectRatePerHour);
this.setInputValue('tpa-waste-factor', state.wasteFactor);
this.setInputValue('tpa-target-margin', state.targetMarginPct);
const geometryEl = document.getElementById('tpa-geometry');
if (geometryEl) geometryEl.value = state.geometry;
const collectionEl = document.getElementById('tpa-collection-filter');
const collections = ['all', ...new Set(this.blanks.map(blank => String(blank.collection || '').trim()).filter(Boolean))];
if (collectionEl) {
const currentHtml = collections.map(collection => collection).join('|');
if (collectionEl.dataset.signature !== currentHtml) {
collectionEl.dataset.signature = currentHtml;
collectionEl.innerHTML = collections.map(collection => {
const label = collection === 'all' ? 'Все коллекции' : collection;
return ``;
}).join('');
}
collectionEl.value = state.collectionFilter;
}
const verdictEl = document.getElementById('tpa-verdict-filter');
if (verdictEl) verdictEl.value = state.verdictFilter;
this.root.querySelectorAll('[data-tpa-preset]').forEach(button => {
button.classList.toggle('active', button.dataset.tpaPreset === state.preset);
});
},
setInputValue(id, value) {
const el = document.getElementById(id);
if (!el) return;
el.value = String(value ?? '');
},
refreshStaticBlocks() {
const assumptionEl = document.getElementById('tpa-assumption-note');
if (assumptionEl) {
assumptionEl.innerHTML = `Базовая договоренность: все формы здесь считаем как новый клиентский молд. Бланки ниже нужны лишь как наглядные примеры геометрии, чтобы объяснить клиенту: при такой форме и таком тираже логичнее инжектор или уже пора смотреть в сторону ТПА.`;
}
const indirectEl = document.getElementById('tpa-indirect-explainer');
const scenario = this.calculateScenario(this.state);
if (indirectEl) {
indirectEl.innerHTML = `
${formatRub(this.state.indirectRatePerHour)}/ч умножается на все часы партии.
В текущем сценарии это ${this.formatHours(scenario.hoursTotal)}, то есть ${formatRub(scenario.indirectTotal)} косвенных на всю партию или ${formatRub(scenario.indirectPerUnit)} на штуку.
Логика максимально простая: пока оператор у станка, ТПА забирает не только ФОТ, но и всю часовую косвенную нагрузку этого производственного времени.
`;
}
},
refreshResults() {
const scenario = this.calculateScenario(this.state);
const technical = this.getTechnicalAssessment(this.state);
const verdict = this.getEconomicVerdict(scenario, technical);
const verdictEl = document.getElementById('tpa-current-verdict');
if (verdictEl) {
verdictEl.className = `tpa-badge ${verdict.key}`;
verdictEl.textContent = verdict.label;
}
const formulaEl = document.getElementById('tpa-formula-box');
if (formulaEl) {
formulaEl.innerHTML = `
Формула: runtime / шт = пластик + ФОТ + косвенные + запуск партии. Итог / шт: runtime / шт + молд / тираж. Здесь сейчас: ${formatRub(scenario.plasticPerUnit)} + ${formatRub(scenario.fotPerUnit)} + ${formatRub(scenario.indirectPerUnit)} + ${formatRub(scenario.setupPerUnit)} + ${formatRub(scenario.moldPerUnit)} = ${formatRub(scenario.totalPerUnit)}.
`;
}
const selectedBlankEl = document.getElementById('tpa-selected-blank');
if (selectedBlankEl) {
if (this.state.selectedBlankId == null) {
selectedBlankEl.style.display = 'none';
selectedBlankEl.innerHTML = '';
} else {
const blank = this.blanks.find(item => String(item.id) === String(this.state.selectedBlankId));
const assessment = blank ? this.assessBlank(blank) : null;
selectedBlankEl.style.display = '';
selectedBlankEl.innerHTML = blank
? `Сейчас выбран референс: ${this.escape(blank.name)}. Мы берем его как понятную геометрию для клиента: ${assessment ? this.escape(assessment.shortWhy) : ''}`
: '';
}
}
const resultsEl = document.getElementById('tpa-results');
if (resultsEl) {
resultsEl.innerHTML = [
this.renderResultCard('Производительность', `${this.formatQty(scenario.piecesPerHour)} шт/ч`, `${this.state.cavities} гнезд × ${this.formatQty(this.state.openingsPerHour)} открытий/ч`),
this.renderResultCard('Часы партии', this.formatHours(scenario.hoursTotal), `1 смена в месяц: ${this.formatQty(scenario.oneShiftMonthly)} шт`),
this.renderResultCard('Runtime без формы', formatRub(scenario.runtimePerUnit), `${formatRub(scenario.runtimeTotal)} на всю партию`),
this.renderResultCard('Форма на 1 шт', formatRub(scenario.moldPerUnit), `${formatRub(this.state.moldCost)} / ${this.formatQty(this.state.quantity)} шт`),
this.renderResultCard('Себестоимость', formatRub(scenario.totalPerUnit), `${formatRub(scenario.totalCost)} на весь тираж`),
this.renderResultCard('Цена без НДС', scenario.sellNoVat > 0 ? formatRub(scenario.sellNoVat) : '—', scenario.sellWithVat > 0 ? `с НДС: ${formatRub(scenario.sellWithVat)}` : 'проверьте целевую маржу'),
].join('');
}
const alertsEl = document.getElementById('tpa-alerts');
if (alertsEl) {
alertsEl.innerHTML = this.getScenarioAlerts(scenario, technical, verdict).map(alert => (
`
`;
},
getVerdictReason(scenario, technical, verdict) {
if (verdict.key === 'injector') {
if (technical.key === 'bad') {
return 'Здесь клиенту проще всего объяснять, что проблема не только в цене формы, но и в самой геометрии для этого станка.';
}
return `Сейчас форму приходится размазывать как минимум на ${formatRub(scenario.moldPerUnit)} / шт, и она душит экономику сильнее, чем сам runtime.`;
}
if (verdict.key === 'transition') {
return 'Это уже разговор не “нет”, а “смотря какой тираж”. По геометрии шанс есть, но форма на штуку еще чувствуется.';
}
return 'Здесь клиенту уже можно уверенно показывать ТПА как рабочий сценарий: геометрия подходит, а форма на штуку перестает быть главным убийцей экономики.';
},
assessBlank(blank) {
const name = String(blank.name || '').trim();
const lower = name.toLowerCase();
const collection = String(blank.collection || '').trim().toLowerCase();
const weight = Number(blank.tpaWeight || 0);
if (collection === 'nfc' || lower.includes('nfc')) {
return this.makeBlankAssessment('Вставка внутри детали', 'Для NFC нужна ручная операция внутри процесса. Для XPM-17 это сразу ломает идею “человек не стоит все время”.', 'insert', 1, 1, 20, 'Нужна вставка / ручное участие', 'Референс для объяснения, почему тут лучше не обещать ТПА.', 'bad');
}
if (lower.includes('карабин')) {
return this.makeBlankAssessment('Толстый монолит', 'Карабин это как раз хороший пример, почему не вся “простая” форма подходит ТПА. Здесь мешает толщина и монолитная масса, а не просто контур.', 'thick', 1, 2, 40, '1–2', 'Хороший пример, почему карабины лучше аргументировать через инжектор или через серьезный редизайн.', 'bad');
}
if (lower.includes('зеркало')) {
return this.makeBlankAssessment('После литья нужна ручная сборка', 'По самому молду еще можно спорить, но зеркало потом надо вклеивать. Это уже не чистый безлюдный ТПА-кейс.', 'insert', 1, 2, 30, '1–2', 'Хороший пример переходной формы: литье возможно, но ручная сборка съедает смысл.', 'bad');
}
if (/(бегов|кроссовк|мыльниц|кардхолдер|картхолдер|подставка|ракетк|падл|велосипед|волчок|гребень|смайл|тюльпан|змея|ласт|большой дракон|большой конь|лошадь большая)/.test(lower)) {
return this.makeBlankAssessment('Слишком большая или толстая геометрия', 'Это уже зона крупных монолитных форм. Для такого XPM-17 она слишком тяжелая и слишком “долго остывающая”.', 'thick', 1, 1, 25, '1', 'Удобный пример для клиента, почему размер сам по себе еще не делает кейс подходящим для ТПА.', 'bad');
}
if (collection === 'буквы' || lower.includes('буква')) {
return this.makeBlankAssessment('Мелкая легкая деталь', 'Буквы показывают сильную сторону ТПА: маленький вес, высокая гнездность и длинные повторяемые серии.', 'small', 6, 8, 70, '6–8', 'Здесь ТПА объясняется клиенту очень легко: много гнезд и длинные тиражи.', 'good');
}
if (lower.includes('бусин')) {
return this.makeBlankAssessment('Мелкая серийная геометрия', 'Бусины и похожие мелкие элементы идеально объясняют, зачем вообще нужен ТПА: компактная форма, много гнезд и ровный цикл.', 'small', 6, 8, 70, '6–8', 'Классический аргумент в пользу ТПА.', 'good');
}
if (lower.includes('тэг') || lower.includes('бирк')) {
return this.makeBlankAssessment('Плоский массовый бланк', 'Тэг это одна из самых показательных форм: маленький вес, понятный контур и очень хороший потенциал по гнездности.', 'flat', 6, 8, 65, '6–8', 'Клиенту легко показать, почему здесь ТПА становится интересным рано.', 'good');
}
if (lower.includes('ключ')) {
return this.makeBlankAssessment('Легкий плоский контур', 'Ключ хороший референс для ТПА: маленький вес и понятная плоская геометрия позволяют думать даже про высокую гнездность.', 'flat', 6, 8, 65, '6–8', 'Сильный кейс для объяснения преимуществ ТПА.', 'good');
}
if (lower.includes('отельн')) {
return this.makeBlankAssessment('Плоская форма средней площади', 'Отельный уже не такой миниатюрный, как ключ или тэг, но все еще остается хорошим кандидатом для 4 гнезд и больших тиражей.', 'flat', 4, 4, 60, '4', 'Хороший референс для форм, которые уже побольше, но еще очень живые для ТПА.', 'good');
}
if (lower.includes('прямоуголь')) {
return this.makeBlankAssessment('Плоская базовая геометрия', 'Прямоугольник это почти идеальный учебный кейс для клиента: простая геометрия, понятная масса и ясная логика по гнездности.', 'flat', 4, 5, 60, '4–5', 'Один из самых наглядных примеров, где ТПА раскрывается.', 'good');
}
if (lower.includes('квадрат')) {
return this.makeBlankAssessment('Плоская легкая форма', 'Квадрат тоже хорошо работает как референс: вес небольшой, форма читаемая, гнездность можно поднимать.', 'flat', 4, 6, 60, '4–6', 'Простой способ показать клиенту силу ТПА на плоских изделиях.', 'good');
}
if (lower.includes('круг')) {
return this.makeBlankAssessment('Плоская компактная форма', 'Круг прекрасно объясняет механику ТПА: маленький вес, аккуратный контур и хороший шанс на 4–6 гнезд.', 'flat', 4, 6, 60, '4–6', 'Хороший учебный пример для клиента.', 'good');
}
if (lower.includes('сердц')) {
return this.makeBlankAssessment('Плоская, но уже декоративная', 'Сердце показывает, что даже неидеально квадратная форма может отлично жить на ТПА, если она тонкая и легкая.', 'flat', 4, 6, 60, '4–6', 'Показывает клиенту, что важна не только форма контура, а именно толщина и вес.', 'good');
}
if (lower.includes('треуголь')) {
return this.makeBlankAssessment('Плоская очень легкая форма', 'Треугольник при таком весе отлично объясняет, зачем нужна гнездность: легкая деталь быстро превращает ТПА в серийную машину.', 'flat', 4, 6, 60, '4–6', 'Очень наглядный ТПА-кандидат.', 'good');
}
if (lower.includes('конверт')) {
return this.makeBlankAssessment('Плоская, но уже крупная по площади', 'Конверт полезен как промежуточный референс: форма все еще плоская, но площадь детали уже начинает съедать комфорт по компоновке.', 'flat', 2, 4, 55, '2–4', 'Хорошо показывает клиенту, что площадь детали тоже влияет на экономику ТПА.', 'transition');
}
if ((lower.includes('цветок') && lower.includes('бланк')) || lower.includes('цветоч')) {
return this.makeBlankAssessment('Плоская форма со сложным контуром', 'Контур сложнее прямоугольника, но если деталь тонкая и легкая, ТПА все еще остается рабочим сценарием.', 'flat', 4, 4, 55, '4', 'Полезный переходный пример между простым бланком и декоративной фигуркой.', 'transition');
}
if (lower.includes('шар')) {
return this.makeBlankAssessment('Округлая объемная деталь', 'Шаром удобно объяснять границу: деталь уже не плоская, остывание сложнее, а значит ТПА становится куда капризнее.', 'ribbed', 2, 2, 45, '2', 'Переходная зона между понятным плоским бланком и спорной объемной формой.', 'transition');
}
if (lower.includes('маленьк') || lower.includes('снежин') || lower.includes('елоч') || lower.includes('цветоч') || lower.includes('сердеч')) {
return this.makeBlankAssessment('Мелкая декоративная форма', 'Небольшие фигурки можно использовать как хороший аргумент в пользу ТПА, если они остаются легкими и не становятся толстыми.', 'small', 4, 6, 55, '4–6', 'Хороший пример для небольших декоративных тиражей.', 'good');
}
if (weight > 0 && weight <= 4) {
return this.makeBlankAssessment('Очень легкая деталь', 'При таком весе клиенту обычно легко показать, почему на длинном тираже ТПА начинает выигрывать по штуке.', 'flat', 4, 6, 60, '4–6', 'Вес уже говорит в пользу ТПА.', 'good');
}
if (weight > 0 && weight <= 7) {
return this.makeBlankAssessment('Легкая плоская деталь', 'По массе это еще комфортная территория для XPM-17, особенно если форма не толстая и без вставок.', 'flat', 4, 5, 60, '4–5', 'Нормальный пример для объяснения перехода в ТПА.', 'good');
}
if (weight > 0 && weight <= 12) {
return this.makeBlankAssessment('Уже ощутимая по массе форма', 'Это еще не “нет”, но клиенту уже нужно объяснять, что выигрыш ТПА придет только на нормальном тираже и без лишней толщины.', 'flat', 2, 4, 50, '2–4', 'Переходный сценарий.', 'transition');
}
return this.makeBlankAssessment('Форма тяжелее среднего', 'Если форма не маленькая и уже тяжелая, XPM-17 быстро теряет свои преимущества. Тут проще продавать инжектор или другой станок.', 'thick', 1, 2, 40, '1–2', 'Удобный пример, почему не все стоит вести в ТПА.', 'bad');
},
makeBlankAssessment(title, shortWhy, geometry, minCavities, maxCavities, openingsPerHour, cavitiesLabel, note, technicalKey = '') {
const autoTechnical = this.getTechnicalAssessment({ geometry, cavities: minCavities, weight: 0 });
const technical = technicalKey
? {
...autoTechnical,
key: technicalKey,
reason: shortWhy,
}
: autoTechnical;
return {
title,
shortWhy,
geometry,
recommendedCavities: minCavities,
openingsPerHour,
cavitiesLabel,
shortNote: note,
technical,
};
},
roundTo5(value) {
return Math.round((Number(value) || 0) / 5) * 5;
},
formatQty(value) {
return new Intl.NumberFormat('ru-RU').format(Math.round(Number(value) || 0));
},
formatHours(value) {
const numeric = round2(Number(value || 0));
return `${numeric.toLocaleString('ru-RU')} ч`;
},
escape(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
},
escapeAttr(value) {
return this.escape(value);
},
};