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

351 lines
13 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">Загрузите файл (TSV/CSV - разделитель Tab или запятая)</label>
<input type="file" id="fileUpload" accept=".tsv,.csv,.txt">
<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>
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) {
const content = e.target.result;
const lines = content.split('\n').filter(line => line.trim());
// Skip header row if exists (check if first line contains "ФИО" or similar)
const startIndex = lines[0].includes('ФИО') || lines[0].includes('Табельный') ? 1 : 0;
let loadedCount = 0;
for (let i = startIndex; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue;
// Detect separator: tab or comma
const separator = line.includes('\t') ? '\t' : ',';
const columns = line.split(separator).map(col => col.trim());
// Expected structure based on user's columns:
// 0: Табельный номер
// 1: ФИО
// 2: Штатная должность
// 3: Грейд
// 4: МВЗ
// 5: ОрганизационнЕдиница
// 6: Текст Орг.Единицы
// 7: Факт-дни
// 8: Оклад по ШР
// 9: Оклад факт
// 10: ДатаВзыскания
// 11: ВидВзыскания
// 12: ДатаПриема
if (columns.length < 13) {
console.log(`Строка ${i+1}: недостаточно столбцов (${columns.length}), пропускаем`);
continue;
}
const fullName = columns[1];
const workedDays = parseInt(columns[7]);
const salary = parseFloat(columns[9].replace(/\s/g, ''));
const hireDateRaw = columns[12];
// Parse hire date (format: DD.MM.YYYY or YYYY-MM-DD)
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 {
console.log(`Строка ${i+1}: неверный формат даты "${hireDateRaw}", пропускаем`);
continue;
}
if (!fullName || isNaN(workedDays) || isNaN(salary)) {
console.log(`Строка ${i+1}: некорректные данные, пропускаем`);
continue;
}
workers.push({
id: Date.now() + i,
fullName,
hireDate,
salary,
workedDays,
reportMonth
});
loadedCount++;
}
updateTable();
alert(`Загружено ${loadedCount} работников`);
fileInput.value = '';
};
reader.readAsText(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>