bonus-calculator/index.html
2026-06-19 06:41:02 +00:00

370 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Калькулятор премий</title>
<style>
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7}
*{box-sizing:border-box;margin:0;padding:0}
body{font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--white)}
.container{max-width:1140px;margin:0 auto;padding:80px 24px}
.hero{background:var(--ink);color:var(--white)}
.hero h1{font-size:56px;font-weight:800;line-height:1.05;margin-bottom:24px}
.hero p{font-size:20px;color:#9aa3b2;max-width:600px;margin-bottom:32px}
.btn{display:inline-block;background:var(--cyan);color:var(--ink);padding:14px 28px;border-radius:8px;font-weight:700;text-decoration:none}
.btn:hover{background:#1be5ff}
.section h2{font-size:36px;font-weight:700;margin-bottom:24px}
.card{background:var(--gray-100);border-radius:16px;padding:32px;margin-bottom:16px}
@media (max-width:640px){.hero h1{font-size:36px}.section h2{font-size:28px}.container{padding:48px 20px}}
.form-group{margin-bottom:16px}
.form-group label{display:block;margin-bottom:8px;font-weight:600}
.form-group input,.form-group select{width:100%;padding:12px;border:2px solid var(--gray-100);border-radius:8px;font-size:16px}
.form-group input:focus,.form-group select:focus{outline:none;border-color:var(--cyan)}
.btn-add{background:var(--cyan);color:var(--ink);padding:12px 24px;border:none;border-radius:8px;font-weight:700;cursor:pointer;margin-bottom:24px}
.btn-add:hover{background:#1be5ff}
.table-container{overflow-x:auto}
table{width:100%;border-collapse:collapse;margin-top:16px}
th,td{padding:12px;text-align:left;border-bottom:2px solid var(--gray-100)}
th{background:var(--gray-100);font-weight:600}
.btn-remove{background:#ff4757;color:white;border:none;padding:6px 12px;border-radius:6px;cursor:pointer}
.btn-remove:hover{background:#ff3344}
.result-section{margin-top:32px;padding:24px;background:var(--cyan-50);border-radius:12px}
.result-section h3{margin-bottom:16px}
.result-item{display:flex;justify-content:space-between;margin-bottom:8px;padding:8px;border-bottom:1px solid var(--gray-100)}
.result-item.excluded{color:#ff4757}
.result-item.included{color:#2ed573}
.total{font-weight:700;font-size:20px;margin-top:16px;padding-top:16px;border-top:2px solid var(--ink)}
</style>
</head>
<body>
<section class="hero">
<div class="container">
<h1>Калькулятор премий</h1>
<p>Автоматический расчет премий с учетом испытательного срока. Исключает работников с незаконченным испытательным сроком и пересчитывает фактические рабочие дни.</p>
<a class="btn" href="#calculator">Начать расчет</a>
</div>
</section>
<section id="calculator" class="section">
<div class="container">
<h2>Добавьте работников</h2>
<div class="card">
<h3 style="margin-bottom:16px">Загрузка списка работников</h3>
<div class="form-group">
<label for="fileUpload">Загрузите файл Excel (.xlsx)</label>
<input type="file" id="fileUpload" accept=".xlsx,.xls">
<small style="color:var(--gray-500)">Ожидаемые столбцы: Табельный номер, ФИО, Штатная должность, Грейд, МВЗ, ОрганизационнЕдиница, Текст Орг.Единицы, Факт-дни, Оклад по ШР, Оклад факт, ДатаВзыскания, ВидВзыскания, ДатаПриема (формат: ДД.ММ.ГГГГ)</small>
</div>
<div class="form-group">
<label for="reportMonth">Отчетный месяц</label>
<input type="month" id="reportMonth">
</div>
<button class="btn-add" onclick="loadFromFile()">Загрузить из файла</button>
<div style="margin:24px 0;text-align:center;color:var(--gray-500)">— или добавляйте вручную —</div>
<div class="form-group">
<label for="fullName">ФИО работника</label>
<input type="text" id="fullName" placeholder="Иванов Иван Иванович">
</div>
<div class="form-group">
<label for="hireDate">Дата приема на работу</label>
<input type="date" id="hireDate">
</div>
<div class="form-group">
<label for="salary">Фактический оклад (₽)</label>
<input type="number" id="salary" placeholder="50000">
</div>
<div class="form-group">
<label for="workedDays">Фактически отработанные рабочие дни</label>
<input type="number" id="workedDays" placeholder="22">
</div>
<button class="btn-add" onclick="addWorker()">Добавить работника</button>
</div>
<div class="table-container">
<table id="workersTable">
<thead>
<tr>
<th>ФИО</th>
<th>Дата приема</th>
<th>Оклад (₽)</th>
<th>Отработано дней</th>
<th>Отчетный месяц</th>
<th>Действие</th>
</tr>
</thead>
<tbody id="workersBody">
</tbody>
</table>
</div>
<button class="btn" onclick="calculateBonus()" style="margin-top:24px">Рассчитать премию</button>
<div id="results" class="result-section" style="display:none">
<h3>Результаты расчета</h3>
<div id="resultsList"></div>
<div id="totalResult" class="total"></div>
</div>
</div>
</section>
<script src="https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js"></script>
<script>
let workers = [];
function addWorker() {
const fullName = document.getElementById('fullName').value;
const hireDate = document.getElementById('hireDate').value;
const salary = parseFloat(document.getElementById('salary').value);
const workedDays = parseInt(document.getElementById('workedDays').value);
const reportMonth = document.getElementById('reportMonth').value;
if (!fullName || !hireDate || isNaN(salary) || isNaN(workedDays) || !reportMonth) {
alert('Заполните все поля');
return;
}
workers.push({
id: Date.now(),
fullName,
hireDate,
salary,
workedDays,
reportMonth
});
updateTable();
clearForm();
}
function loadFromFile() {
const fileInput = document.getElementById('fileUpload');
const reportMonth = document.getElementById('reportMonth').value;
if (!fileInput.files.length) {
alert('Выберите файл для загрузки');
return;
}
if (!reportMonth) {
alert('Укажите отчетный месяц');
return;
}
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, {type: 'array'});
// Get first sheet
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
// Convert to JSON
const jsonData = XLSX.utils.sheet_to_json(worksheet, {header: 1});
if (jsonData.length === 0) {
alert('Файл пуст');
return;
}
// Find header row index (look for "ФИО" in any cell)
let headerRowIndex = -1;
for (let i = 0; i < Math.min(5, jsonData.length); i++) {
const row = jsonData[i];
if (row && row.some(cell => cell && cell.toString().includes('ФИО'))) {
headerRowIndex = i;
break;
}
}
if (headerRowIndex === -1) {
// Assume first row is header
headerRowIndex = 0;
}
// Get data rows (skip header)
const dataRows = jsonData.slice(headerRowIndex + 1);
let loadedCount = 0;
for (let i = 0; i < dataRows.length; i++) {
const row = dataRows[i];
// Skip empty rows
if (!row || row.length < 13) {
console.log(`Строка ${i + headerRowIndex + 2}: недостаточно столбцов, пропускаем`);
continue;
}
const fullName = row[1] ? row[1].toString().trim() : '';
const workedDays = row[7] ? parseInt(row[7]) : NaN;
const salary = row[9] ? parseFloat(row[9].toString().replace(/\s/g, '')) : NaN;
const hireDateRaw = row[12] ? row[12].toString().trim() : '';
// Parse hire date (format: DD.MM.YYYY or Excel date)
let hireDate;
if (hireDateRaw.includes('.')) {
const [day, month, year] = hireDateRaw.split('.');
hireDate = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
} else if (hireDateRaw.includes('-')) {
hireDate = hireDateRaw;
} else if (typeof row[12] === 'number') {
// Excel date serial number
const excelDate = new Date((row[12] - 25569) * 86400 * 1000);
hireDate = excelDate.toISOString().split('T')[0];
} else {
console.log(`Строка ${i + headerRowIndex + 2}: неверный формат даты "${hireDateRaw}", пропускаем`);
continue;
}
if (!fullName || isNaN(workedDays) || isNaN(salary)) {
console.log(`Строка ${i + headerRowIndex + 2}: некорректные данные, пропускаем`);
continue;
}
workers.push({
id: Date.now() + i,
fullName,
hireDate,
salary,
workedDays,
reportMonth
});
loadedCount++;
}
updateTable();
alert(`Загружено ${loadedCount} работников`);
fileInput.value = '';
} catch (error) {
console.error('Ошибка чтения файла:', error);
alert('Ошибка чтения файла. Убедитесь, что файл в формате Excel (.xlsx)');
}
};
reader.readAsArrayBuffer(file);
}
function removeWorker(id) {
workers = workers.filter(w => w.id !== id);
updateTable();
}
function updateTable() {
const tbody = document.getElementById('workersBody');
tbody.innerHTML = workers.map(w => `
<tr>
<td>${w.fullName}</td>
<td>${formatDate(w.hireDate)}</td>
<td>${w.salary.toLocaleString('ru-RU')}</td>
<td>${w.workedDays}</td>
<td>${formatMonth(w.reportMonth)}</td>
<td><button class="btn-remove" onclick="removeWorker(${w.id})">Удалить</button></td>
</tr>
`).join('');
}
function clearForm() {
document.getElementById('fullName').value = '';
document.getElementById('hireDate').value = '';
document.getElementById('salary').value = '';
document.getElementById('workedDays').value = '';
document.getElementById('reportMonth').value = '';
}
function formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString('ru-RU');
}
function formatMonth(monthStr) {
const [year, month] = monthStr.split('-');
const months = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
return `${months[parseInt(month)-1]} ${year}`;
}
function calculateBonus() {
if (workers.length === 0) {
alert('Добавьте хотя бы одного работника');
return;
}
const results = [];
let totalBonus = 0;
workers.forEach(worker => {
const hireDate = new Date(worker.hireDate);
const probationEndDate = new Date(hireDate);
probationEndDate.setMonth(probationEndDate.getMonth() + 1);
const [reportYear, reportMonth] = worker.reportMonth.split('-').map(Number);
const reportEndDate = new Date(reportYear, reportMonth, 0);
const isProbationEnded = probationEndDate <= reportEndDate;
let includedDays = 0;
let bonusAmount = 0;
let excludedReason = '';
if (!isProbationEnded) {
excludedReason = `Испытательный срок до ${formatDate(probationEndDate.toISOString().split('T')[0])}`;
} else {
const daysInMonth = reportEndDate.getDate();
const daysBeforeProbation = Math.max(0, Math.ceil((probationEndDate - new Date(reportYear, reportMonth-1, 1)) / (1000*60*60*24)));
includedDays = daysInMonth - daysBeforeProbation;
const dailyRate = worker.salary / daysInMonth;
bonusAmount = dailyRate * includedDays;
}
results.push({
...worker,
isProbationEnded,
includedDays,
bonusAmount,
excludedReason,
probationEndDate: formatDate(probationEndDate.toISOString().split('T')[0])
});
totalBonus += bonusAmount;
});
displayResults(results, totalBonus);
}
function displayResults(results, totalBonus) {
const resultsDiv = document.getElementById('results');
const resultsList = document.getElementById('resultsList');
const totalResult = document.getElementById('totalResult');
resultsList.innerHTML = results.map(r => `
<div class="result-item ${r.isProbationEnded ? 'included' : 'excluded'}">
<div>
<strong>${r.fullName}</strong><br>
<small>${r.isProbationEnded ? `Включен: ${r.includedDays} дней` : `Исключен: ${r.excludedReason}`}</small>
</div>
<div>${r.isProbationEnded ? r.bonusAmount.toLocaleString('ru-RU') + ' ₽' : '—'}</div>
</div>
`).join('');
totalResult.innerHTML = `Итого к выплате: ${totalBonus.toLocaleString('ru-RU')}`;
resultsDiv.style.display = 'block';
}
</script>
</body>
</html>