444 lines
14 KiB
JavaScript
444 lines
14 KiB
JavaScript
// === Хранилище данных ===
|
||
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 =>
|
||
`<option value="${t.id}">${escapeHtml(t.name)}</option>`
|
||
).join('');
|
||
|
||
if (state.currentTypeId) {
|
||
select.value = state.currentTypeId;
|
||
}
|
||
}
|
||
|
||
// === Рендер табов в настройках ===
|
||
function renderTypeTabs() {
|
||
const container = document.getElementById('typeTabs');
|
||
container.innerHTML = state.settings.types.map(t =>
|
||
`<button class="tab-btn${t.id === state.currentTypeId ? ' active' : ''}"
|
||
data-type-id="${t.id}">${escapeHtml(t.name)}</button>`
|
||
).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 `
|
||
<tr>
|
||
<td>${dateStr}<br><small style="color: #71717a">${timeStr}</small></td>
|
||
<td>${escapeHtml(record.typeName)}</td>
|
||
<td>${formatNumber(record.speed, 4)} Мбит/с</td>
|
||
<td><strong>${formatNumber(record.total)} ₸</strong></td>
|
||
</tr>
|
||
`;
|
||
}).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();
|