// === Хранилище данных ===
const STORAGE_KEYS = {
SETTINGS: 'costCalculator_settings',
HISTORY: 'costCalculator_history',
CURRENT_TYPE: 'costCalculator_currentType'
};
// Данные по умолчанию
const DEFAULT_SETTINGS = {
types: [
{
id: 'regional',
name: 'Областной',
varCosts: 15921.2583,
avgCapacity: 9.4317,
fixedCosts: 8129.6707
}
]
};
// Состояние приложения
let state = {
settings: null,
history: [],
currentTypeId: null
};
// === Инициализация ===
function init() {
loadSettings();
loadHistory();
renderTypeSelector();
renderTypeTabs();
renderHistory();
setupEventListeners();
// Загружаем последний выбранный тип
const lastType = localStorage.getItem(STORAGE_KEYS.CURRENT_TYPE);
if (lastType && state.settings.types.find(t => t.id === lastType)) {
state.currentTypeId = lastType;
document.getElementById('calcType').value = lastType;
}
}
// === Загрузка настроек ===
function loadSettings() {
const saved = localStorage.getItem(STORAGE_KEYS.SETTINGS);
if (saved) {
try {
state.settings = JSON.parse(saved);
} catch (e) {
state.settings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
}
} else {
state.settings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
}
if (!state.currentTypeId && state.settings.types.length > 0) {
state.currentTypeId = state.settings.types[0].id;
}
}
// === Сохранение настроек ===
function saveSettings() {
localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(state.settings));
}
// === Загрузка истории ===
function loadHistory() {
const saved = localStorage.getItem(STORAGE_KEYS.HISTORY);
if (saved) {
try {
state.history = JSON.parse(saved);
} catch (e) {
state.history = [];
}
}
}
// === Сохранение истории ===
function saveHistory() {
localStorage.setItem(STORAGE_KEYS.HISTORY, JSON.stringify(state.history));
}
// === Рендер селектора типов ===
function renderTypeSelector() {
const select = document.getElementById('calcType');
select.innerHTML = state.settings.types.map(t =>
``
).join('');
if (state.currentTypeId) {
select.value = state.currentTypeId;
}
}
// === Рендер табов в настройках ===
function renderTypeTabs() {
const container = document.getElementById('typeTabs');
container.innerHTML = state.settings.types.map(t =>
``
).join('');
}
// === Получение текущего типа ===
function getCurrentType() {
return state.settings.types.find(t => t.id === state.currentTypeId);
}
// === Заполнение формы настроек ===
function fillSettingsForm() {
const type = getCurrentType();
if (!type) return;
document.getElementById('typeName').value = type.name;
document.getElementById('varCosts').value = type.varCosts;
document.getElementById('avgCapacity').value = type.avgCapacity;
document.getElementById('fixedCosts').value = type.fixedCosts;
// Показываем кнопку удаления, если типов больше 1
document.getElementById('deleteTypeBtn').style.display =
state.settings.types.length > 1 ? 'inline-block' : 'none';
}
// === Расчет себестоимости ===
function calculateCost(speed, type) {
const variablePart = (type.varCosts / type.avgCapacity) * speed;
const total = variablePart + type.fixedCosts;
return {
variablePart,
fixedPart: type.fixedCosts,
total,
speed,
type
};
}
// === Форматирование числа ===
function formatNumber(num, decimals = 2) {
return num.toLocaleString('ru-RU', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
});
}
// === Экранирование HTML ===
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// === Валидация ===
function validateSettings(name, varCosts, avgCapacity, fixedCosts, isEditing = false) {
const errors = [];
if (!name || name.trim() === '') {
errors.push('Введите название типа расчета');
}
const existingType = state.settings.types.find(t =>
t.name.toLowerCase() === name.toLowerCase().trim() && t.id !== (isEditing ? state.currentTypeId : null)
);
if (existingType) {
errors.push('Тип с таким названием уже существует');
}
if (varCosts === '' || isNaN(varCosts) || parseFloat(varCosts) < 0) {
errors.push('Переменные затраты должны быть неотрицательным числом');
}
if (avgCapacity === '' || isNaN(avgCapacity) || parseFloat(avgCapacity) <= 0) {
errors.push('Пропускная способность должна быть положительным числом');
}
if (fixedCosts === '' || isNaN(fixedCosts) || parseFloat(fixedCosts) < 0) {
errors.push('Постоянные затраты должны быть неотрицательным числом');
}
return errors;
}
function validateSpeed(speed) {
if (speed === '' || speed === null || speed === undefined) {
return 'Введите необходимую скорость';
}
const num = parseFloat(speed);
if (isNaN(num) || num < 0) {
return 'Скорость не может быть отрицательной';
}
return null;
}
// === Добавление записи в историю ===
function addToHistory(result, type) {
const record = {
id: Date.now(),
date: new Date().toISOString(),
typeId: type.id,
typeName: type.name,
speed: result.speed,
varCosts: type.varCosts,
avgCapacity: type.avgCapacity,
fixedCosts: type.fixedCosts,
total: result.total
};
state.history.unshift(record);
// Храним последние 100 записей
if (state.history.length > 100) {
state.history = state.history.slice(0, 100);
}
saveHistory();
}
// === Рендер истории ===
function renderHistory() {
const tbody = document.getElementById('historyBody');
const emptyMsg = document.getElementById('emptyHistory');
if (state.history.length === 0) {
tbody.innerHTML = '';
emptyMsg.classList.add('show');
return;
}
emptyMsg.classList.remove('show');
tbody.innerHTML = state.history.map(record => {
const date = new Date(record.date);
const dateStr = date.toLocaleDateString('ru-RU');
const timeStr = date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
return `
${dateStr} ${timeStr} |
${escapeHtml(record.typeName)} |
${formatNumber(record.speed, 4)} Мбит/с |
${formatNumber(record.total)} ₸ |
`;
}).join('');
}
// === Очистка истории ===
function clearHistory() {
if (confirm('Вы уверены, что хотите очистить всю историю расчетов?')) {
state.history = [];
saveHistory();
renderHistory();
}
}
// === Показать результат ===
function showResult(result, type) {
const section = document.getElementById('resultSection');
document.getElementById('resultValue').textContent = formatNumber(result.total) + ' ₸';
document.getElementById('variablePart').textContent = formatNumber(result.variablePart) + ' ₸';
document.getElementById('fixedPart').textContent = formatNumber(result.fixedPart) + ' ₸';
document.getElementById('speedValue').textContent = formatNumber(result.speed, 4) + ' Мбит/с';
// Формула
const formula = `(${formatNumber(type.varCosts, 4)} / ${formatNumber(type.avgCapacity, 4)}) × ${formatNumber(result.speed, 4)} + ${formatNumber(type.fixedCosts, 4)} = ${formatNumber(result.total)} ₸`;
document.getElementById('formulaText').textContent = formula;
section.classList.add('show');
section.style.display = 'block';
section.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// === Setup event listeners ===
function setupEventListeners() {
// Калькулятор
document.getElementById('calculateBtn').addEventListener('click', handleCalculate);
document.getElementById('calcType').addEventListener('change', (e) => {
state.currentTypeId = e.target.value;
localStorage.setItem(STORAGE_KEYS.CURRENT_TYPE, state.currentTypeId);
});
// История
document.getElementById('clearHistory').addEventListener('click', clearHistory);
// Модальное окно настроек
document.getElementById('openSettings').addEventListener('click', openSettings);
document.getElementById('closeSettings').addEventListener('click', closeSettings);
document.getElementById('settingsModal').addEventListener('click', (e) => {
if (e.target.id === 'settingsModal') closeSettings();
});
// Табы и кнопки в настройках
document.getElementById('typeTabs').addEventListener('click', handleTabClick);
document.getElementById('addTypeBtn').addEventListener('click', addNewType);
document.getElementById('saveSettings').addEventListener('click', saveSettingsHandler);
document.getElementById('deleteTypeBtn').addEventListener('click', deleteType);
// Enter в поле скорости
document.getElementById('speed').addEventListener('keypress', (e) => {
if (e.key === 'Enter') handleCalculate();
});
// Enter в полях настроек
document.getElementById('typeName').addEventListener('keypress', (e) => {
if (e.key === 'Enter') saveSettingsHandler();
});
}
// === Обработчик расчета ===
function handleCalculate() {
const speedInput = document.getElementById('speed');
const speed = parseFloat(speedInput.value);
const validationError = validateSpeed(speedInput.value);
if (validationError) {
alert(validationError);
speedInput.focus();
return;
}
const type = getCurrentType();
if (!type) {
alert('Выберите тип расчета');
return;
}
// Проверка на деление на ноль
if (type.avgCapacity <= 0) {
alert('Ошибка: пропускная способность должна быть больше нуля. Проверьте настройки.');
openSettings();
return;
}
const result = calculateCost(speed, type);
showResult(result, type);
addToHistory(result, type);
renderHistory();
}
// === Открытие настроек ===
function openSettings() {
renderTypeTabs();
fillSettingsForm();
document.getElementById('saveMessage').style.display = 'none';
document.getElementById('settingsModal').style.display = 'flex';
document.getElementById('typeName').focus();
}
// === Закрытие настроек ===
function closeSettings() {
document.getElementById('settingsModal').style.display = 'none';
}
// === Обработчик клика по табам ===
function handleTabClick(e) {
if (!e.target.classList.contains('tab-btn')) return;
const typeId = e.target.dataset.typeId;
state.currentTypeId = typeId;
renderTypeTabs();
fillSettingsForm();
}
// === Добавление нового типа ===
function addNewType() {
const newId = 'type_' + Date.now();
const newType = {
id: newId,
name: 'Новый тип',
varCosts: 0,
avgCapacity: 1,
fixedCosts: 0
};
state.settings.types.push(newType);
state.currentTypeId = newId;
saveSettings();
renderTypeTabs();
fillSettingsForm();
}
// === Удаление типа ===
function deleteType() {
if (state.settings.types.length <= 1) {
alert('Нельзя удалить последний тип расчета');
return;
}
if (confirm('Вы уверены, что хотите удалить этот тип расчета?')) {
const index = state.settings.types.findIndex(t => t.id === state.currentTypeId);
state.settings.types.splice(index, 1);
// Выбираем первый оставшийся тип
state.currentTypeId = state.settings.types[0].id;
saveSettings();
renderTypeSelector();
renderTypeTabs();
fillSettingsForm();
}
}
// === Сохранение настроек ===
function saveSettingsHandler() {
const name = document.getElementById('typeName').value.trim();
const varCosts = parseFloat(document.getElementById('varCosts').value);
const avgCapacity = parseFloat(document.getElementById('avgCapacity').value);
const fixedCosts = parseFloat(document.getElementById('fixedCosts').value);
const errors = validateSettings(name, varCosts, avgCapacity, fixedCosts, true);
if (errors.length > 0) {
alert(errors.join('\n'));
return;
}
const type = getCurrentType();
type.name = name;
type.varCosts = varCosts;
type.avgCapacity = avgCapacity;
type.fixedCosts = fixedCosts;
saveSettings();
renderTypeSelector();
renderTypeTabs();
// Показываем сообщение об успехе
const msg = document.getElementById('saveMessage');
msg.style.display = 'block';
setTimeout(() => {
msg.style.display = 'none';
}, 3000);
}
// === Закрытие модального окна по Escape ===
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeSettings();
}
});
// === Старт ===
init();