Untitled
1 hour ago in Plain Text
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Калькулятор PRO</title>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Калькулятор PRO">
<meta name="theme-color" content="#ffffff">
<link id="manifest-link" rel="manifest">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script>
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
</script>
<style>
body { font-family: 'Inter', sans-serif; -webkit-tap-highlight-color: transparent; }
:root { --bg-main: #F9FAFB; --bg-card: #FFFFFF; --text-primary: #111827; --text-secondary: #6B7280; --border-color: #E5E7EB; --brand-color: #10B981; --brand-color-dark: #059669; --toggle-bg: #D1D5DB; }
.dark { --bg-main: #111827; --bg-card: #1F2937; --text-primary: #F9FAFB; --text-secondary: #9CA3AF; --border-color: #374151; --brand-color: #60A5FA; --brand-color-dark: #2563EB; --toggle-bg: #4B5563; }
body { background-color: var(--bg-main); color: var(--text-primary); transition: background-color 0.3s, color 0.3s; }
.card { background-color: var(--bg-card); border: 1px solid var(--border-color); }
.input-group label { color: var(--text-secondary); }
.input-group input, .input-group select { background-color: var(--bg-card); color: var(--text-primary); border-color: var(--border-color); }
.input-group input:focus, .input-group select:focus { border-color: var(--brand-color); box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-color) 40%, transparent); }
.result-line { border-color: var(--border-color); }
.result-line span:first-child { color: var(--text-secondary); }
.final-payout span { color: #16A34A; }
.dark .final-payout span { color: #4ADE80; }
.brand-header { color: var(--brand-color); }
input[type="range"] { -webkit-appearance: none; appearance: none; width: 100%; height: 8px; background: var(--border-color); border-radius: 5px; outline: none; transition: background 0.3s; }
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 24px; height: 24px; background: var(--brand-color); border-radius: 50%; cursor: pointer; border: 3px solid var(--bg-card); }
input[type="range"]::-moz-range-thumb { width: 24px; height: 24px; background: var(--brand-color); border-radius: 50%; cursor: pointer; border: 3px solid var(--bg-card); }
.toggle-bg { background-color: var(--toggle-bg); }
.peer:checked ~ .toggle-bg { background-color: var(--brand-color); }
</style>
</head>
<body class="bg-[--bg-main]">
<div class="w-full min-h-screen p-4 md:p-6">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-[--text-primary]">Калькулятор PRO</h1>
<button id="theme-toggle" type="button" class="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg text-sm p-2.5">
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
<svg id="theme-toggle-light-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm-.707 8.486a1 1 0 011.414 0l.707.707a1 1 0 01-1.414 1.414l-.707-.707a1 1 0 010-1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div id="input-form" class="card rounded-2xl p-6 space-y-4"></div>
<div id="results-container"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const workDaysByMonth2025 = [ { month: 'Январь', days: 21 }, { month: 'Февраль', days: 20 }, { month: 'Март', days: 18 }, { month: 'Апрель', days: 22 }, { month: 'Май', days: 20 }, { month: 'Июнь', days: 20 }, { month: 'Июль', days: 22 }, { month: 'Август', days: 20 }, { month: 'Сентябрь', days: 21 }, { month: 'Октябрь', days: 22 }, { month: 'Ноябрь', days: 20 }, { month: 'Декабрь', days: 21 } ];
const initialData = { oklad: 393754, days_worked: workDaysByMonth2025[8].days, sovmeshchenie_days: workDaysByMonth2025[8].days, hazardous_days: 0, bonus_percent: 10, sovmeshchenie_max_percent: 30, advance: 77000, mrp: 3932, has_quarterly_bonus: false, quarterly_bonus_amount: 212500, has_sick_leave: false, sick_leave_days: 0 };
const createField = (id, label, value) => `<div class="input-group"><label for="${id}">${label}</label><input type="number" id="${id}" value="${value}" class="text-center font-medium text-lg"></div>`;
const createSelect = (id, label) => { const options = workDaysByMonth2025.map((m, i) => `<option value="${m.days}" ${i === 8 ? 'selected' : ''}>${m.month} (${m.days} дн.)</option>`).join(''); return `<div class="input-group"><label for="${id}">${label}</label><select id="${id}" class="font-medium">${options}</select></div>`; };
const createSlider = (id, label, value, max) => `<div class="input-group"><div class="flex justify-between items-center mb-2"><label for="${id}">${label}</label><span id="${id}-value" class="font-bold text-lg brand-header">${value}</span></div><input type="range" id="${id}" value="${value}" min="0" max="${max}"></div>`;
const createToggle = (id, label, checked) => `<div class="flex items-center justify-between mt-4 p-3 bg-gray-200/50 dark:bg-gray-700/50 rounded-lg"><label for="${id}" class="font-medium cursor-pointer">${label}</label><label class="relative inline-flex items-center cursor-pointer"><input type="checkbox" id="${id}" class="sr-only peer" ${checked ? 'checked' : ''}><div class="w-11 h-6 toggle-bg rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all"></div></label></div>`;
const createResults = () => `<div class="card rounded-2xl p-6 h-full"><h2 class="text-xl font-semibold mb-4 border-b border-[--border-color] pb-3">Итоговый расчет</h2><div><h3 class="font-semibold brand-header mb-2">Начисления</h3><div id="earnings-breakdown"></div><div class="result-line font-bold"><span>Итого начислено:</span><span id="gross_salary" class="brand-header"></span></div></div><div class="mt-6"><h3 class="font-semibold brand-header mb-2">Удержания</h3><div id="deductions-breakdown"></div><div class="result-line font-bold"><span>Итого удержано:</span><span id="total_deductions" class="brand-header"></span></div></div><div class="mt-6"><h3 class="font-semibold brand-header mb-2">К выплате</h3><div class="result-line" id="net_sick_leave_line" style="display: none;"><span>Больничные 'на руки':</span><span id="net_sick_leave" class="font-bold text-[--brand-color-dark]"></span></div><div class="result-line"><span>Итого "на руки":</span><span id="net_salary"></span></div><div class="result-line" id="net_quarterly_bonus_line" style="display: none;"><span>Кварт. премия 'на руки':</span><span id="net_quarterly_bonus" class="font-bold text-[--brand-color-dark]"></span></div><div class="result-line"><span>Выплаченный аванс:</span><span id="paid_advance"></span></div><div class="result-line final-payout mt-2"><span>Окончательный расчет:</span><span id="final_payout"></span></div></div></div>`;
const formatCurrency = (value) => new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'KZT', minimumFractionDigits: 2 }).format(value);
function calculateAndRender() { try { const oklad = parseFloat(document.getElementById('oklad').value) || 0; let days_worked = parseFloat(document.getElementById('days_worked').value) || 0; const sovmeshchenie_days = parseFloat(document.getElementById('sovmeshchenie_days').value) || 0; const hazardous_days = parseFloat(document.getElementById('hazardous_days').value) || 0; const has_quarterly_bonus = document.getElementById('has_quarterly_bonus').checked; const quarterly_bonus_amount = has_quarterly_bonus ? (parseFloat(document.getElementById('quarterly_bonus_amount').value) || 0) : 0; const total_work_days = parseFloat(document.getElementById('month_selector').value) || 1; const has_sick_leave = document.getElementById('has_sick_leave').checked; const sick_leave_days = has_sick_leave ? (parseFloat(document.getElementById('sick_leave_days').value) || 0) : 0; const { bonus_percent, sovmeshchenie_max_percent, advance, mrp } = initialData; if(has_sick_leave) { days_worked = Math.max(0, total_work_days - sick_leave_days); } const okladFact = (oklad / total_work_days) * days_worked; const bonusAmount = oklad * (bonus_percent / 100); const sovmeshchenieAmount = (oklad * (sovmeshchenie_max_percent / 100) / total_work_days) * sovmeshchenie_days; const vutAmount = hazardous_days > 0 ? ((14.9 * mrp) / total_work_days) * hazardous_days : 0; const grossSalaryMain = okladFact + bonusAmount + sovmeshchenieAmount + vutAmount; const opvMain = grossSalaryMain * 0.10; const osmsMain = grossSalaryMain * 0.02; const ipnDeductionBase = 14 * mrp; const ipnTaxableAmountMain = Math.max(0, grossSalaryMain - opvMain - osmsMain - ipnDeductionBase); const ipnMain = ipnTaxableAmountMain * 0.10; const netSalaryMain = grossSalaryMain - (opvMain + osmsMain + ipnMain); let opvBonus = 0, osmsBonus = 0, ipnBonus = 0, netSalaryBonus = 0; if (has_quarterly_bonus && quarterly_bonus_amount > 0) { opvBonus = quarterly_bonus_amount * 0.10; osmsBonus = quarterly_bonus_amount * 0.02; ipnBonus = Math.max(0, quarterly_bonus_amount - opvBonus - osmsBonus) * 0.10; netSalaryBonus = quarterly_bonus_amount - (opvBonus + osmsBonus + ipnBonus); } let grossSickPay = 0, netSickPay = 0, opvSick = 0, osmsSick = 0; if (has_sick_leave && sick_leave_days > 0) { grossSickPay = 25 * mrp; opvSick = grossSickPay * 0.10; osmsSick = grossSickPay * 0.02; netSickPay = grossSickPay - opvSick - osmsSick; } document.getElementById('earnings-breakdown').innerHTML = `<div class="result-line"><span>Оклад (факт):</span><span>${formatCurrency(okladFact)}</span></div>${has_sick_leave && sick_leave_days > 0 ? `<div class="result-line"><span>Больничный (оплата):</span><span class="font-bold text-[--brand-color-dark]">${formatCurrency(grossSickPay)}</span></div>` : ''}<div class="result-line"><span>Премия:</span><span>${formatCurrency(bonusAmount)}</span></div><div class="result-line"><span>Совмещение:</span><span>${formatCurrency(sovmeshchenieAmount)}</span></div><div class="result-line"><span>ВУТ:</span><span>${formatCurrency(vutAmount)}</span></div>${has_quarterly_bonus ? `<div class="result-line"><span>Кварт. премия:</span><span class="font-bold text-[--brand-color-dark]">${formatCurrency(quarterly_bonus_amount)}</span></div>` : ''}`; document.getElementById('gross_salary').textContent = formatCurrency(grossSalaryMain + (has_quarterly_bonus ? quarterly_bonus_amount : 0) + grossSickPay); document.getElementById('deductions-breakdown').innerHTML = `<div class="result-line"><span>ОПВ:</span><span>${formatCurrency(opvMain + opvBonus + opvSick)}</span></div><div class="result-line"><span>ВОСМС:</span><span>${formatCurrency(osmsMain + osmsBonus + osmsSick)}</span></div><div class="result-line"><span>ИПН (с ЗП):</span><span>${formatCurrency(ipnMain + ipnBonus)}</span></div>`; document.getElementById('total_deductions').textContent = formatCurrency(opvMain + osmsMain + ipnMain + opvBonus + osmsBonus + ipnBonus + opvSick + osmsSick); document.getElementById('net_salary').textContent = formatCurrency(netSalaryMain + netSalaryBonus + netSickPay); document.getElementById('paid_advance').textContent = formatCurrency(advance); document.getElementById('final_payout').textContent = formatCurrency(netSalaryMain - advance); const bonusLine = document.getElementById('net_quarterly_bonus_line'); if (has_quarterly_bonus) { document.getElementById('net_quarterly_bonus').textContent = formatCurrency(netSalaryBonus); bonusLine.style.display = 'flex'; } else { bonusLine.style.display = 'none'; } const sickLeaveLine = document.getElementById('net_sick_leave_line'); if (has_sick_leave && sick_leave_days > 0) { document.getElementById('net_sick_leave').textContent = formatCurrency(netSickPay); sickLeaveLine.style.display = 'flex'; } else { sickLeaveLine.style.display = 'none'; } } catch (error) { console.error("Calculation Error:", error); } }
function setupApp() {
const formContainer = document.getElementById('input-form'); const maxDays = workDaysByMonth2025[8].days;
formContainer.innerHTML = `<h2 class="text-xl font-semibold mb-4 border-b border-[--border-color] pb-3">Параметры расчета</h2> ${createField('oklad', 'Полный оклад (₸)', initialData.oklad)} ${createSelect('month_selector', 'Расчетный месяц (2025 г.)')} ${createSlider('days_worked', 'Отработано дней', initialData.days_worked, maxDays)} ${createSlider('sovmeshchenie_days', 'Дней с совмещением', initialData.sovmeshchenie_days, maxDays)} ${createSlider('hazardous_days', 'Дней во вредных условиях', initialData.hazardous_days, maxDays)} ${createToggle('has_quarterly_bonus', 'Квартальная премия', initialData.has_quarterly_bonus)} <div id="quarterly_bonus_container" class="hidden"> ${createField('quarterly_bonus_amount', 'Сумма премии (₸)', initialData.quarterly_bonus_amount)} </div> ${createToggle('has_sick_leave', 'Есть больничный?', initialData.has_sick_leave)} <div id="sick_leave_container" class="hidden space-y-4 pt-4 border-t border-[--border-color]"> ${createSlider('sick_leave_days', 'Дней на больничном', 0, maxDays)} </div> <div class="mt-8 flex justify-end"> <button id="reset-button" class="bg-[--brand-color] text-white font-bold py-2 px-5 rounded-lg shadow-md hover:bg-[--brand-color-dark] focus:outline-none focus:ring-4 focus:ring-color-mix(in srgb, var(--brand-color) 40%, transparent) transition">Сбросить</button> </div>`;
document.getElementById('results-container').innerHTML = createResults();
const daysWorkedSlider = document.getElementById('days_worked'); const sovmeshchenieSlider = document.getElementById('sovmeshchenie_days'); const hazardousSlider = document.getElementById('hazardous_days'); const sickLeaveSlider = document.getElementById('sick_leave_days'); const monthSelector = document.getElementById('month_selector');
formContainer.addEventListener('input', (e) => { if (e.target.type === 'range') { document.getElementById(`${e.target.id}-value`).textContent = e.target.value; } calculateAndRender(); });
monthSelector.addEventListener('change', (e) => { const days = e.target.value; [daysWorkedSlider, sovmeshchenieSlider, hazardousSlider, sickLeaveSlider].forEach(slider => { if(slider) { slider.max = days; if (parseInt(slider.value) > days) slider.value = days; document.getElementById(`${slider.id}-value`).textContent = slider.value; }}); calculateAndRender(); });
document.getElementById('has_quarterly_bonus').addEventListener('change', (e) => { document.getElementById('quarterly_bonus_container').classList.toggle('hidden', !e.target.checked); calculateAndRender(); });
document.getElementById('has_sick_leave').addEventListener('change', (e) => { document.getElementById('sick_leave_container').classList.toggle('hidden', !e.target.checked); if(!e.target.checked) { sickLeaveSlider.value = 0; document.getElementById('sick_leave_days-value').textContent = 0; } calculateAndRender(); });
sickLeaveSlider.addEventListener('input', (e) => { const totalDays = monthSelector.value; const sickDays = e.target.value; const worked = Math.max(0, totalDays - sickDays); daysWorkedSlider.value = worked; document.getElementById('days_worked-value').textContent = worked; });
document.getElementById('reset-button').addEventListener('click', () => { monthSelector.selectedIndex = 8; const selectedMonthDays = workDaysByMonth2025[8].days; [sovmeshchenieSlider].forEach(slider => { slider.value = selectedMonthDays; slider.max = selectedMonthDays; document.getElementById(`${slider.id}-value`).textContent = slider.value;}); [daysWorkedSlider, hazardousSlider, sickLeaveSlider].forEach(slider => { slider.value = slider.id === 'days_worked' ? selectedMonthDays : 0; slider.max = selectedMonthDays; document.getElementById(`${slider.id}-value`).textContent = slider.value;}); ['has_quarterly_bonus', 'has_sick_leave'].forEach(id => document.getElementById(id).checked = false); ['quarterly_bonus_container', 'sick_leave_container'].forEach(id => document.getElementById(id).classList.add('hidden')); document.getElementById('quarterly_bonus_amount').value = initialData.quarterly_bonus_amount; calculateAndRender(); });
const themeToggleBtn = document.getElementById('theme-toggle'); const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon'); const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon'); if (localStorage.getItem('color-theme') === 'dark') { themeToggleLightIcon.classList.remove('hidden'); } else { themeToggleDarkIcon.classList.remove('hidden'); }
themeToggleBtn.addEventListener('click', () => { themeToggleDarkIcon.classList.toggle('hidden'); themeToggleLightIcon.classList.toggle('hidden'); if (document.documentElement.classList.contains('dark')) { document.documentElement.classList.remove('dark'); localStorage.setItem('color-theme', 'light'); } else { document.documentElement.classList.add('dark'); localStorage.setItem('color-theme', 'dark'); } });
calculateAndRender();
}
function setupPWA() { const manifest = { "name": "Калькулятор PRO", "short_name": "Зарплата", "start_url": ".", "display": "standalone", "background_color": "#111827", "theme_color": "#1F2937", "description": "Профессиональный калькулятор зарплаты", "icons": [{"src": "https://www.gstatic.com/images/branding/product/1x/drive_2020q4_48dp.png", "sizes": "192x192", "type": "image/png"},{"src": "https://www.gstatic.com/images/branding/product/1x/drive_2020q4_48dp.png", "sizes": "512x512", "type": "image/png"}] }; const manifestBlob = new Blob([JSON.stringify(manifest)], {type: 'application/json'}); const manifestURL = URL.createObjectURL(manifestBlob); document.querySelector('#manifest-link').setAttribute('href', manifestURL); const swCode = ` const CACHE_NAME = 'calculator-pro-cache-v12'; const urlsToCache = ['.', 'https://cdn.tailwindcss.com', 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap']; self.addEventListener('install', e => e.waitUntil(caches.open(CACHE_NAME).then(c => c.addAll(urlsToCache)))); self.addEventListener('fetch', e => e.respondWith(caches.match(e.request).then(r => r || fetch(e.request)))); `; const swBlob = new Blob([swCode], {type: 'application/javascript'}); const swUrl = URL.createObjectURL(swBlob); if ('serviceWorker' in navigator) { navigator.serviceWorker.register(swUrl); } }
setupApp();
setupPWA();
});
</script>
</body>
</html>