267 lines
9.5 KiB
JavaScript
267 lines
9.5 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function() {
|
||
const form = document.getElementById('wfmForm');
|
||
const resultsSection = document.getElementById('results');
|
||
const historyList = document.getElementById('historyList');
|
||
const uploadArea = document.getElementById('uploadArea');
|
||
const fileInput = document.getElementById('fileInput');
|
||
const uploadStatus = document.getElementById('uploadStatus');
|
||
const monthlyResults = document.getElementById('monthlyResults');
|
||
const tabs = document.querySelectorAll('.tab');
|
||
const modes = {
|
||
manual: document.getElementById('manualMode'),
|
||
upload: document.getElementById('uploadMode'),
|
||
monthly: document.getElementById('monthlyMode')
|
||
};
|
||
|
||
tabs.forEach(tab => {
|
||
tab.addEventListener('click', function() {
|
||
const mode = this.dataset.mode;
|
||
tabs.forEach(t => t.classList.remove('active'));
|
||
this.classList.add('active');
|
||
Object.keys(modes).forEach(k => modes[k].classList.remove('active'));
|
||
modes[mode].classList.add('active');
|
||
if (mode === 'monthly') {
|
||
loadMonthlyResults();
|
||
}
|
||
});
|
||
});
|
||
|
||
form.addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
const formData = new FormData(form);
|
||
const params = {};
|
||
formData.forEach((value, key) => {
|
||
if (value !== '') {
|
||
params[key] = parseFloat(value);
|
||
}
|
||
});
|
||
|
||
try {
|
||
const response = await fetch('/api/calculate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(params)
|
||
});
|
||
|
||
const results = await response.json();
|
||
displayResults(results);
|
||
loadHistory();
|
||
|
||
resultsSection.style.display = 'block';
|
||
resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
} catch (error) {
|
||
alert('Ошибка расчёта: ' + error.message);
|
||
}
|
||
});
|
||
|
||
uploadArea.addEventListener('click', () => fileInput.click());
|
||
|
||
uploadArea.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.add('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('dragleave', () => {
|
||
uploadArea.classList.remove('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove('dragover');
|
||
const files = e.dataTransfer.files;
|
||
if (files.length > 0) {
|
||
handleFile(files[0]);
|
||
}
|
||
});
|
||
|
||
fileInput.addEventListener('change', (e) => {
|
||
if (e.target.files.length > 0) {
|
||
handleFile(e.target.files[0]);
|
||
}
|
||
});
|
||
|
||
async function handleFile(file) {
|
||
uploadStatus.innerHTML = '<div class="kt-ai-card" style="padding:var(--kt-ai-space-4)">Загрузка файла...</div>';
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
try {
|
||
const response = await fetch('/api/upload', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.ok) {
|
||
uploadStatus.innerHTML = '<div class="kt-ai-card" style="padding:var(--kt-ai-space-4);background:var(--kt-ai-bg-soft)">✅ Файл загружен! Найдено месяцев: ' + result.months.length + '</div>';
|
||
|
||
if (result.months.length > 0) {
|
||
displayMonthlyResults(result.months, result.globalParams);
|
||
tabs[2].click();
|
||
}
|
||
} else {
|
||
uploadStatus.innerHTML = '<div class="kt-ai-card" style="padding:var(--kt-ai-space-4);color:var(--kt-ai-danger)">Ошибка: ' + result.error + '</div>';
|
||
}
|
||
} catch (error) {
|
||
uploadStatus.innerHTML = '<div class="kt-ai-card" style="padding:var(--kt-ai-space-4);color:var(--kt-ai-danger)">Ошибка загрузки: ' + error.message + '</div>';
|
||
}
|
||
}
|
||
|
||
function displayResults(results) {
|
||
if (results.incoming) {
|
||
document.getElementById('resIncoming').textContent = results.incoming.fte;
|
||
document.getElementById('resIncomingSL').textContent = 'SL: ' + results.incoming.sl + '%';
|
||
} else {
|
||
document.getElementById('resIncoming').textContent = '-';
|
||
document.getElementById('resIncomingSL').textContent = '';
|
||
}
|
||
|
||
if (results.outgoing) {
|
||
document.getElementById('resOutgoing').textContent = results.outgoing.fte;
|
||
} else {
|
||
document.getElementById('resOutgoing').textContent = '-';
|
||
}
|
||
|
||
if (results.tickets) {
|
||
document.getElementById('resTickets').textContent = results.tickets.fte;
|
||
} else {
|
||
document.getElementById('resTickets').textContent = '-';
|
||
}
|
||
|
||
if (results.chats) {
|
||
document.getElementById('resChats').textContent = results.chats.fte;
|
||
} else {
|
||
document.getElementById('resChats').textContent = '-';
|
||
}
|
||
|
||
document.getElementById('resTotalSchedule').textContent = results.total.fteInSchedule.toFixed(1);
|
||
document.getElementById('resTotalWithAbsence').textContent = results.total.fteWithAbsence.toFixed(1);
|
||
}
|
||
|
||
function displayMonthlyResults(months, params) {
|
||
const totalFTE = months.reduce((sum, m) => sum + m.results.total.fteWithAbsence, 0);
|
||
const avgFTE = totalFTE / months.length;
|
||
|
||
let html = `
|
||
<div class="wfm-card" style="margin-bottom:var(--kt-ai-space-6)">
|
||
<h3 style="margin-bottom:var(--kt-ai-space-4)">Итого за период</h3>
|
||
<div class="results-grid">
|
||
<div class="result-item">
|
||
<div class="result-value">${avgFTE.toFixed(1)}</div>
|
||
<div class="result-label">Среднее FTE в штате</div>
|
||
</div>
|
||
<div class="result-item">
|
||
<div class="result-value">${totalFTE.toFixed(1)}</div>
|
||
<div class="result-label">Суммарно FTE (все месяцы)</div>
|
||
</div>
|
||
<div class="result-item">
|
||
<div class="result-value">${months.length}</div>
|
||
<div class="result-label">Месяцев в расчёте</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wfm-card">
|
||
<h3 style="margin-bottom:var(--kt-ai-space-4)">Детализация по месяцам</h3>
|
||
<div style="overflow-x:auto">
|
||
<table class="monthly-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Месяц</th>
|
||
<th>Входящие</th>
|
||
<th>Исходящие</th>
|
||
<th>Тикеты</th>
|
||
<th>Чаты</th>
|
||
<th>FTE в графике</th>
|
||
<th>FTE в штате</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
months.forEach(m => {
|
||
html += `
|
||
<tr>
|
||
<td><strong>${m.month}</strong></td>
|
||
<td>${m.incomingCalls.toLocaleString('ru-RU')}</td>
|
||
<td>${m.outgoingCalls ? m.outgoingCalls.toLocaleString('ru-RU') : '-'}</td>
|
||
<td>${m.tickets ? m.tickets.toLocaleString('ru-RU') : '-'}</td>
|
||
<td>${m.chats ? m.chats.toLocaleString('ru-RU') : '-'}</td>
|
||
<td style="color:var(--kt-ai-fg)">${m.results.total.fteInSchedule.toFixed(1)}</td>
|
||
<td style="color:var(--kt-ai-primary);font-weight:var(--kt-ai-weight-semibold)">${m.results.total.fteWithAbsence.toFixed(1)}</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
monthlyResults.innerHTML = html;
|
||
}
|
||
|
||
async function loadHistory() {
|
||
try {
|
||
const response = await fetch('/api/history');
|
||
const history = await response.json();
|
||
|
||
if (history.length === 0) {
|
||
historyList.innerHTML = '<div class="kt-ai-empty">История пуста</div>';
|
||
return;
|
||
}
|
||
|
||
historyList.innerHTML = history.map(calc => {
|
||
const date = new Date(calc.date).toLocaleString('ru-RU');
|
||
const total = calc.results.total.fteWithAbsence.toFixed(1);
|
||
return `
|
||
<div class="kt-ai-card" style="padding:var(--kt-ai-space-4);margin-bottom:var(--kt-ai-space-3)">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<div>
|
||
<strong>${total} FTE</strong>
|
||
<span style="color:var(--kt-ai-fg-muted);margin-left:var(--kt-ai-space-2);font-size:var(--kt-ai-text-sm)">${date}</span>
|
||
</div>
|
||
<div style="font-size:var(--kt-ai-text-sm);color:var(--kt-ai-fg-muted)">
|
||
Вх: ${calc.results.incoming ? calc.results.incoming.fte : '-'} |
|
||
Исх: ${calc.results.outgoing ? calc.results.outgoing.fte : '-'} |
|
||
Чаты: ${calc.results.chats ? calc.results.chats.fte : '-'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки истории:', error);
|
||
}
|
||
}
|
||
|
||
async function loadMonthlyResults() {
|
||
try {
|
||
const response = await fetch('/api/monthly');
|
||
const data = await response.json();
|
||
|
||
if (!data || data.length === 0) {
|
||
monthlyResults.innerHTML = `
|
||
<div class="wfm-card">
|
||
<h3>Нет данных</h3>
|
||
<p style="color:var(--kt-ai-fg-muted)">Загрузите Excel файл во вкладке "Загрузить Excel" для расчёта по месяцам</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
displayMonthlyResults(data, {});
|
||
} catch (error) {
|
||
console.error('Ошибка:', error);
|
||
monthlyResults.innerHTML = '<div class="kt-ai-card" style="color:var(--kt-ai-danger)">Ошибка загрузки данных</div>';
|
||
}
|
||
}
|
||
|
||
loadHistory();
|
||
});
|