293 lines
11 KiB
JavaScript
293 lines
11 KiB
JavaScript
// Данные для расчёта
|
||
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);
|
||
});
|