// === Хранилище данных === 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();