459 lines
17 KiB
HTML
459 lines
17 KiB
HTML
<!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> |