bonus-calculator/index.html
2026-06-19 07:08:15 +00:00

459 lines
17 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>
<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>
<button class="btn" onclick="exportToExcel()" style="margin-top:16px">Выгрузить в Excel</button>
</div>
</div>
</section>
<script src="https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js"></script>
<script>
let workers = [];
let originalData = []; // Store original Excel data for export
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
});
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;
}
// Store original data for export
originalData = jsonData;
// 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++;
}
alert(`Загружено ${loadedCount} работников`);
fileInput.value = '';
} catch (error) {
console.error('Ошибка чтения файла:', error);
alert('Ошибка чтения файла. Убедитесь, что файл в формате Excel (.xlsx)');
}
};
reader.readAsArrayBuffer(file);
}
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);
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear().toString().slice(-2);
return `${day}.${month}.${year}`;
}
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 reportStartDate = new Date(reportYear, reportMonth - 1, 1);
const reportEndDate = new Date(reportYear, reportMonth, 0);
const daysInMonth = reportEndDate.getDate();
let includedDays = 0;
let recalculatedSalary = 0;
let excludedReason = '';
let isExcluded = false;
// Check if probation ends after the report month
if (probationEndDate > reportEndDate) {
isExcluded = true;
excludedReason = `Испытательный срок до ${formatDate(probationEndDate.toISOString().split('T')[0])}`;
} else {
// Calculate days after probation ends
let daysAfterProbation;
if (probationEndDate <= reportStartDate) {
// Probation ended before the report month started - full month
daysAfterProbation = daysInMonth;
} else {
// Probation ends during the report month
const probationEndDay = probationEndDate.getDate();
daysAfterProbation = daysInMonth - probationEndDay + 1;
}
// Recalculate worked days and salary proportionally
includedDays = Math.round((worker.workedDays / daysInMonth) * daysAfterProbation * 100) / 100;
recalculatedSalary = Math.round((worker.salary / daysInMonth) * daysAfterProbation * 100) / 100;
}
results.push({
...worker,
isExcluded,
includedDays,
recalculatedSalary,
excludedReason,
probationEndDate: formatDate(probationEndDate.toISOString().split('T')[0]),
daysAfterProbation: isExcluded ? 0 : (probationEndDate <= reportStartDate ? daysInMonth : daysInMonth - probationEndDate.getDate() + 1)
});
if (!isExcluded) {
totalBonus += recalculatedSalary;
}
});
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.isExcluded ? 'excluded' : 'included'}">
<div>
<strong>${r.fullName}</strong><br>
<small>${r.isExcluded ?
`Исключен: ${r.excludedReason}` :
`Включен: ${r.includedDays} дней (пересчет с ${r.probationEndDate}), оклад ${r.recalculatedSalary.toLocaleString('ru-RU')}`
}</small>
</div>
<div>${r.isExcluded ? '—' : r.recalculatedSalary.toLocaleString('ru-RU') + ' ₸'}</div>
</div>
`).join('');
totalResult.innerHTML = `Итого к выплате: ${totalBonus.toLocaleString('ru-RU')}`;
resultsDiv.style.display = 'block';
// Store results for export
window.lastResults = results;
}
function exportToExcel() {
if (!window.lastResults || window.lastResults.length === 0) {
alert('Сначала выполните расчет');
return;
}
if (originalData.length === 0) {
alert('Нет исходных данных для экспорта');
return;
}
const results = window.lastResults;
// Create new workbook
const wb = XLSX.utils.book_new();
// Prepare data for export
// Start with original data (header + data rows)
const exportData = [];
// Find header row index
let headerRowIndex = 0;
for (let i = 0; i < Math.min(5, originalData.length); i++) {
const row = originalData[i];
if (row && row.some(cell => cell && cell.toString().includes('ФИО'))) {
headerRowIndex = i;
break;
}
}
// Add header rows (before actual data header)
for (let i = 0; i <= headerRowIndex; i++) {
const row = originalData[i] ? [...originalData[i]] : [];
if (i === headerRowIndex) {
// Add new column headers to the header row
row.push('Факт-дни (перерасчет)', 'Оклад факт (перерасчет)', 'Примечание');
}
exportData.push(row);
}
// Add data rows with results
const dataRows = originalData.slice(headerRowIndex + 1);
for (let i = 0; i < dataRows.length; i++) {
const row = dataRows[i] ? [...dataRows[i]] : [];
// Find matching result by worker name (column 1)
const workerName = row[1] ? row[1].toString().trim() : '';
const result = results.find(r => r.fullName === workerName);
if (result) {
if (result.isExcluded) {
row.push(0, 0, `Исключен: ${result.excludedReason}`);
} else {
row.push(
result.includedDays,
result.recalculatedSalary,
`Пересчет с ${result.probationEndDate}`
);
}
} else {
row.push('', '', '');
}
exportData.push(row);
}
// Create worksheet
const ws = XLSX.utils.aoa_to_sheet(exportData);
// Set column widths
ws['!cols'] = [
{wch: 15}, // Табельный номер
{wch: 30}, // ФИО
{wch: 25}, // Штатная должность
{wch: 10}, // Грейд
{wch: 15}, // МВЗ
{wch: 20}, // ОрганизационнЕдиница
{wch: 25}, // Текст Орг.Единицы
{wch: 12}, // Факт-дни
{wch: 15}, // Оклад по ШР
{wch: 15}, // Оклад факт
{wch: 15}, // ДатаВзыскания
{wch: 20}, // ВидВзыскания
{wch: 15}, // ДатаПриема
{wch: 18}, // Факт-дни (перерасчет)
{wch: 18}, // Оклад факт (перерасчет)
{wch: 30} // Примечание
];
// Add worksheet to workbook
XLSX.utils.book_append_sheet(wb, ws, 'Результат');
// Generate file and download
const fileName = `Премии_${new Date().toISOString().slice(0, 10)}.xlsx`;
XLSX.writeFile(wb, fileName);
}
</script>
</body>
</html>