sozday-kalkulyator-po-sozdan/script.js

293 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Данные для расчёта
let calculationData = {};
// Форматирование числа в тенге
function formatTenge(num) {
return new Intl.NumberFormat('kk-KZ', {
style: 'currency',
currency: 'KZT',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(num);
}
// Расчёт стоимости
function calculate() {
// Параметры клиента
const users = parseInt(document.getElementById('users').value) || 0;
const term = parseInt(document.getElementById('term').value) || 1;
const serviceType = document.getElementById('serviceType').value;
const minutes = parseInt(document.getElementById('minutes').value) || 0;
const dataGb = parseInt(document.getElementById('dataGb').value) || 0;
const addons = document.getElementById('addons').value;
// Затраты компании
const costMinute = parseFloat(document.getElementById('costMinute').value) || 0;
const costGb = parseFloat(document.getElementById('costGb').value) || 0;
const opex = parseFloat(document.getElementById('opex').value) || 0;
const margin = parseFloat(document.getElementById('margin').value) || 0;
let volumeDiscount = parseFloat(document.getElementById('volumeDiscount').value) || 0;
let termDiscount = parseFloat(document.getElementById('termDiscount').value) || 0;
// Автоматические скидки
if (users >= 100) {
volumeDiscount = Math.max(volumeDiscount, 10);
}
if (term >= 12) {
termDiscount = Math.max(termDiscount, 5);
}
if (term >= 24) {
termDiscount = Math.max(termDiscount, 10);
}
// Расчёт себестоимости на пользователя в месяц
let serviceCost = 0;
switch (serviceType) {
case 'mobile':
serviceCost = (minutes * costMinute) + opex;
break;
case 'internet':
serviceCost = (dataGb * costGb) + opex;
break;
case 'combined':
serviceCost = (minutes * costMinute) + (dataGb * costGb) + opex;
break;
case 'cloud':
serviceCost = (dataGb * costGb * 1.5) + opex; // Облака дороже
break;
}
// Добавочная стоимость за сервисы
let addonCost = 0;
switch (addons) {
case 'basic':
addonCost = 500;
break;
case 'advanced':
addonCost = 1200;
break;
case 'premium':
addonCost = 2500;
break;
}
const totalCostPerUser = serviceCost + addonCost;
// Расчёт цены с маржой
const priceWithMargin = totalCostPerUser * (1 + margin / 100);
// Применение скидок
const totalDiscount = Math.min(volumeDiscount + termDiscount, 40); // Максимум 40%
const finalPrice = priceWithMargin * (1 - totalDiscount / 100);
// Данные конкурентов
const competitors = [
{ name: 'Kazakhtelecom', base: parseFloat(document.getElementById('comp1Base').value) || 0, biz: parseFloat(document.getElementById('comp1Biz').value) || 0, prem: parseFloat(document.getElementById('comp1Prem').value) || 0 },
{ name: 'Kcell/Altel', base: parseFloat(document.getElementById('comp2Base').value) || 0, biz: parseFloat(document.getElementById('comp2Biz').value) || 0, prem: parseFloat(document.getElementById('comp2Prem').value) || 0 },
{ name: 'Beeline', base: parseFloat(document.getElementById('comp3Base').value) || 0, biz: parseFloat(document.getElementById('comp3Biz').value) || 0, prem: parseFloat(document.getElementById('comp3Prem').value) || 0 },
{ name: 'Tele2', base: parseFloat(document.getElementById('comp4Base').value) || 0, biz: parseFloat(document.getElementById('comp4Biz').value) || 0, prem: parseFloat(document.getElementById('comp4Prem').value) || 0 }
];
// Средние цены конкурентов
const avgCompetitors = competitors.reduce((sum, c) => sum + c.biz, 0) / competitors.length;
// Итого
const totalMonthly = finalPrice * users;
const totalContract = totalMonthly * term;
// Сохранение данных
calculationData = {
users,
term,
serviceType,
minutes,
dataGb,
addons,
costMinute,
costGb,
opex,
margin,
volumeDiscount,
termDiscount,
totalCostPerUser,
finalPrice,
avgCompetitors,
totalMonthly,
totalContract,
competitors
};
// Вывод результатов
document.getElementById('resultCost').textContent = formatTenge(totalCostPerUser);
document.getElementById('resultPrice').textContent = formatTenge(finalPrice);
document.getElementById('resultCompetitor').textContent = formatTenge(avgCompetitors);
document.getElementById('resultTotal').textContent = formatTenge(totalMonthly);
document.getElementById('resultContract').textContent = formatTenge(totalContract);
// Конкурентное преимущество
const advantage = ((avgCompetitors - finalPrice) / avgCompetitors * 100).toFixed(1);
const advantageEl = document.getElementById('resultAdvantage');
if (advantage > 0) {
advantageEl.textContent = `+${advantage}% дешевле конкурентов`;
advantageEl.style.color = '#48bb78';
} else if (advantage < 0) {
advantageEl.textContent = `${advantage}% дороже конкурентов`;
advantageEl.style.color = '#f56565';
} else {
advantageEl.textContent = 'На уровне рынка';
advantageEl.style.color = '#666';
}
// Отрисовка сравнения
renderComparison(finalPrice, competitors);
// Рекомендации
renderRecommendations(finalPrice, avgCompetitors, margin, totalDiscount);
}
// Отрисовка сравнения с конкурентами
function renderComparison(ourPrice, competitors) {
const container = document.getElementById('comparisonBars');
const maxValue = Math.max(ourPrice, ...competitors.map(c => c.biz)) * 1.2;
let html = `
<div class="comp-bar-item">
<div class="comp-bar-label">Наш тариф</div>
<div class="comp-bar-track">
<div class="comp-bar-fill ours" style="width: ${(ourPrice / maxValue * 100)}%">
${formatTenge(ourPrice)}
</div>
</div>
<div class="comp-bar-value">${formatTenge(ourPrice)}</div>
</div>
`;
const compClasses = ['comp1', 'comp2', 'comp3', 'comp4'];
competitors.forEach((comp, i) => {
html += `
<div class="comp-bar-item">
<div class="comp-bar-label">${comp.name}</div>
<div class="comp-bar-track">
<div class="comp-bar-fill ${compClasses[i]}" style="width: ${(comp.biz / maxValue * 100)}%">
${formatTenge(comp.biz)}
</div>
</div>
<div class="comp-bar-value">${formatTenge(comp.biz)}</div>
</div>
`;
});
container.innerHTML = html;
}
// Рекомендации
function renderRecommendations(price, avgCompetitorPrice, margin, discount) {
const container = document.getElementById('recommendations');
const recommendations = [];
if (price > avgCompetitorPrice * 1.1) {
recommendations.push('Цена выше средней по рынку на 10%+. Рассмотрите возможность снижения маржинальности или увеличения скидок.');
} else if (price < avgCompetitorPrice * 0.9) {
recommendations.push('Цена ниже рынка — можно увеличить маржу на 5-10% без потери конкурентоспособности.');
} else {
recommendations.push('Цена находится в рыночном диапазоне — оптимально для привлечения клиентов.');
}
if (margin < 15) {
recommendations.push('Низкая маржинальность. Убедитесь, что операционные расходы учтены полностью.');
} else if (margin > 40) {
recommendations.push('Высокая маржинальность. Проверьте, соответствует ли ценность услуги такой наценке.');
}
if (discount === 0) {
recommendations.push('Скидки не применяются. Рассмотрите возможность предоставления скидки за объём или срок.');
}
if (discount >= 30) {
recommendations.push('Высокий уровень скидок. Убедитесь, что маржинальность остаётся приемлемой.');
}
const status = price <= avgCompetitorPrice ? 'Конкурентоспособно' : 'Требуется оптимизация';
const statusColor = price <= avgCompetitorPrice ? '#48bb78' : '#f56565';
container.innerHTML = `
<h3>💡 Рекомендации <span style="color: ${statusColor}; font-size: 14px;">(${status})</span></h3>
<ul>
${recommendations.map(r => `<li>${r}</li>`).join('')}
</ul>
`;
}
// Экспорт в Excel (CSV с BOM)
function exportToExcel() {
if (!calculationData.finalPrice) {
alert('Сначала выполните расчёт!');
return;
}
const d = calculationData;
// CSV с BOM для корректной кириллицы
const bom = '\uFEFF';
const separator = ';';
const rows = [
['ПАРАМЕТРЫ КЛИЕНТА'],
['Количество пользователей', d.users],
['Срок подключения (месяцев)', d.term],
['Тип услуги', d.serviceType],
['Объём минут', d.minutes],
['Объём интернета (ГБ)', d.dataGb],
['Дополнительные сервисы', d.addons],
[],
['ЗАТРАТЫ КОМПАНИИ'],
['Себестоимость минуты (₸)', d.costMinute],
['Себестоимость 1 ГБ (₸)', d.costGb],
['Операционные расходы (₸)', d.opex],
['Маржинальность (%)', d.margin],
['Скидка за объём (%)', d.volumeDiscount],
['Скидка за срок (%)', d.termDiscount],
[],
['РЕЗУЛЬТАТЫ РАСЧЁТА'],
['Себестоимость на пользователя (₸)', d.totalCostPerUser.toFixed(2)],
['Рекомендованная цена (₸)', d.finalPrice.toFixed(2)],
['Средняя цена конкурентов (₸)', d.avgCompetitors.toFixed(2)],
['Итого в месяц (₸)', d.totalMonthly.toFixed(2)],
['Итого за контракт (₸)', d.totalContract.toFixed(2)],
[],
['КОНКУРЕНТЫ (Бизнес тариф)'],
['Kazakhtelecom', d.competitors[0].biz],
['Kcell/Altel', d.competitors[1].biz],
['Beeline', d.competitors[2].biz],
['Tele2', d.competitors[3].biz]
];
const csv = rows.map(row =>
row.map(cell => {
const str = String(cell ?? '');
return '"' + str.replace(/"/g, '""') + '"';
}).join(separator)
).join('\n');
const blob = new Blob([bom + csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `tarif_${new Date().toISOString().slice(0,10)}.csv`;
a.click();
URL.revokeObjectURL(url);
}
// Event listeners
document.getElementById('calculateBtn').addEventListener('click', calculate);
document.getElementById('exportBtn').addEventListener('click', exportToExcel);
// Автоматический расчёт при загрузке
window.addEventListener('DOMContentLoaded', calculate);
// Пересчёт при изменении полей
document.querySelectorAll('input, select').forEach(el => {
el.addEventListener('change', calculate);
});