928 lines
31 KiB
JavaScript
928 lines
31 KiB
JavaScript
// Pulse App - Main JavaScript
|
||
|
||
// Инициализация при загрузке
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initNavigation();
|
||
initRatingInputs();
|
||
loadDashboard();
|
||
loadIdeas();
|
||
loadProfile();
|
||
initForms();
|
||
initFilters();
|
||
initModals();
|
||
renderCharts();
|
||
loadMotivation();
|
||
loadKPI();
|
||
loadReserve();
|
||
});
|
||
|
||
// Навигация между секциями
|
||
function initNavigation() {
|
||
const navLinks = document.querySelectorAll('.nav-link');
|
||
navLinks.forEach(link => {
|
||
link.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
const section = link.dataset.section;
|
||
showSection(section);
|
||
|
||
navLinks.forEach(l => l.classList.remove('active'));
|
||
link.classList.add('active');
|
||
});
|
||
});
|
||
}
|
||
|
||
function showSection(sectionId) {
|
||
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
|
||
document.getElementById(sectionId).classList.add('active');
|
||
|
||
if (sectionId === 'analytics') {
|
||
loadAnalytics();
|
||
}
|
||
}
|
||
|
||
window.showSection = showSection;
|
||
|
||
// Рейтинг (звезды)
|
||
function initRatingInputs() {
|
||
const ratingContainers = document.querySelectorAll('.rating-input');
|
||
|
||
ratingContainers.forEach(container => {
|
||
const stars = container.querySelectorAll('.rating-star');
|
||
const hiddenInput = container.parentElement.querySelector('input[type="hidden"]');
|
||
|
||
stars.forEach(star => {
|
||
star.addEventListener('click', () => {
|
||
const value = parseInt(star.dataset.value);
|
||
stars.forEach((s, i) => {
|
||
s.classList.toggle('active', i < value);
|
||
});
|
||
if (hiddenInput) {
|
||
hiddenInput.value = value;
|
||
}
|
||
container.dataset.value = value;
|
||
});
|
||
|
||
star.addEventListener('mouseenter', () => {
|
||
const value = parseInt(star.dataset.value);
|
||
stars.forEach((s, i) => {
|
||
s.classList.toggle('active', i < value);
|
||
});
|
||
});
|
||
|
||
star.addEventListener('mouseleave', () => {
|
||
const currentValue = parseInt(container.dataset.value || 0);
|
||
stars.forEach((s, i) => {
|
||
s.classList.toggle('active', i < currentValue);
|
||
});
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
// Загрузка дэшборда
|
||
function loadDashboard() {
|
||
const reviews = appData.reviews;
|
||
const ideas = appData.ideas;
|
||
|
||
// KPI
|
||
document.getElementById('kpiReviews').textContent = reviews.length;
|
||
|
||
const avgRating = reviews.length > 0
|
||
? (reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length).toFixed(1)
|
||
: '0.0';
|
||
document.getElementById('kpiAvgRating').textContent = avgRating;
|
||
|
||
document.getElementById('kpiIdeas').textContent = ideas.length;
|
||
document.getElementById('kpiImplemented').textContent = ideas.filter(i => i.status === 'done').length;
|
||
document.getElementById('kpiResolved').textContent = reviews.filter(r => r.status === 'done').length;
|
||
|
||
const engagementIndex = appData.settings.engagementIndex;
|
||
document.getElementById('kpiEngagement').textContent = engagementIndex;
|
||
|
||
const trendEl = document.getElementById('kpiTrend');
|
||
const trend = appData.settings.engagementTrend;
|
||
trendEl.textContent = trend === 'up' ? '↑ рост' : trend === 'down' ? '↓ снижение' : '→ без изменений';
|
||
|
||
// Последние идеи
|
||
const ideasList = document.getElementById('ideasList');
|
||
const recentIdeas = ideas.slice(0, 3);
|
||
ideasList.innerHTML = recentIdeas.map(idea => createIdeaCard(idea)).join('');
|
||
}
|
||
|
||
// Создание карточки идеи
|
||
function createIdeaCard(idea) {
|
||
const statusLabels = {
|
||
new: 'Новая',
|
||
review: 'На рассмотрении',
|
||
accepted: 'Принята',
|
||
implement: 'В реализации',
|
||
done: 'Реализована',
|
||
rejected: 'Отклонена'
|
||
};
|
||
|
||
return `
|
||
<div class="idea-card" data-id="${idea.id}">
|
||
<div class="idea-header">
|
||
<div class="idea-title">${idea.title}</div>
|
||
<span class="idea-status status-${idea.status}">${statusLabels[idea.status]}</span>
|
||
</div>
|
||
<div class="idea-meta">
|
||
<span>👤 ${idea.anonymous ? 'Анонимно' : idea.author}</span>
|
||
<span>📅 ${formatDate(idea.date)}</span>
|
||
<span>🏢 ${getDeptName(idea.department)}</span>
|
||
</div>
|
||
<div class="idea-description">
|
||
<strong>Проблема:</strong> ${idea.problem}<br>
|
||
<strong>Решение:</strong> ${idea.solution}
|
||
</div>
|
||
<div class="idea-actions">
|
||
<button class="vote-btn" onclick="voteIdea(${idea.id})">
|
||
👍 <span class="vote-count">${idea.votes}</span>
|
||
</button>
|
||
<span class="idea-comments">💬 ${idea.comments} комментариев</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Загрузка идей
|
||
function loadIdeas() {
|
||
const container = document.getElementById('ideasContainer');
|
||
container.innerHTML = appData.ideas.map(idea => createIdeaCard(idea)).join('');
|
||
}
|
||
|
||
// Голосование за идею
|
||
function voteIdea(id) {
|
||
const idea = appData.ideas.find(i => i.id === id);
|
||
if (idea && !idea.hasVoted) {
|
||
idea.votes++;
|
||
idea.hasVoted = true;
|
||
saveData(appData);
|
||
loadIdeas();
|
||
|
||
// Визуальное подтверждение
|
||
const btn = event.target.closest('.vote-btn');
|
||
btn.classList.add('voted');
|
||
btn.querySelector('.vote-count').textContent = idea.votes;
|
||
}
|
||
}
|
||
|
||
window.voteIdea = voteIdea;
|
||
|
||
// Фильтры идей
|
||
function initFilters() {
|
||
const filterBtns = document.querySelectorAll('.filter-btn');
|
||
filterBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
filterBtns.forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
|
||
const status = btn.dataset.status;
|
||
const container = document.getElementById('ideasContainer');
|
||
|
||
const filtered = status === 'all'
|
||
? appData.ideas
|
||
: appData.ideas.filter(i => i.status === status);
|
||
|
||
container.innerHTML = filtered.map(idea => createIdeaCard(idea)).join('');
|
||
});
|
||
});
|
||
}
|
||
|
||
// Формы
|
||
function initForms() {
|
||
// Форма обратной связи
|
||
document.getElementById('feedbackForm').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
|
||
const review = {
|
||
id: Date.now(),
|
||
date: new Date().toISOString().split('T')[0],
|
||
direction: document.getElementById('fbDirection').value,
|
||
rating: parseInt(document.getElementById('fbRating').value) || 0,
|
||
type: document.getElementById('fbType').value,
|
||
comment: document.getElementById('fbComment').value,
|
||
suggestion: document.getElementById('fbSuggestion').value,
|
||
anonymous: document.getElementById('fbAnonymous').checked,
|
||
department: appData.profile.department.toLowerCase().split(' ')[0],
|
||
status: 'new'
|
||
};
|
||
|
||
appData.reviews.push(review);
|
||
saveData(appData);
|
||
|
||
document.getElementById('feedbackForm').style.display = 'none';
|
||
document.getElementById('feedbackSuccess').style.display = 'block';
|
||
|
||
loadDashboard();
|
||
loadProfile();
|
||
});
|
||
|
||
// Форма идеи
|
||
document.getElementById('ideaForm').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
|
||
const idea = {
|
||
id: Date.now(),
|
||
title: document.getElementById('ideaTitle').value,
|
||
problem: document.getElementById('ideaProblem').value,
|
||
solution: document.getElementById('ideaSolution').value,
|
||
effect: document.getElementById('ideaEffect').value,
|
||
category: document.getElementById('ideaCategory').value,
|
||
department: document.getElementById('ideaDept').value,
|
||
votes: 0,
|
||
comments: 0,
|
||
status: 'new',
|
||
anonymous: document.getElementById('ideaAnonymous').checked,
|
||
author: appData.profile.name,
|
||
date: new Date().toISOString().split('T')[0],
|
||
hasVoted: false
|
||
};
|
||
|
||
appData.ideas.push(idea);
|
||
saveData(appData);
|
||
|
||
closeModal('ideaModal');
|
||
loadIdeas();
|
||
loadDashboard();
|
||
|
||
alert('Идея отправлена на рассмотрение!');
|
||
e.target.reset();
|
||
});
|
||
|
||
// Форма благодарности
|
||
document.getElementById('thankFormEl').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
|
||
const thank = {
|
||
id: Date.now(),
|
||
to: document.getElementById('thankTo').value,
|
||
from: document.getElementById('thankAnonymous').checked ? 'Анонимно' : appData.profile.name,
|
||
reason: document.getElementById('thankReason').value,
|
||
type: document.getElementById('thankType').value,
|
||
date: new Date().toISOString().split('T')[0],
|
||
anonymous: document.getElementById('thankAnonymous').checked
|
||
};
|
||
|
||
appData.motivation.thanks.unshift(thank);
|
||
saveData(appData);
|
||
|
||
document.getElementById('thankForm').style.display = 'none';
|
||
loadMotivation();
|
||
alert('Благодарность отправлена!');
|
||
});
|
||
|
||
// Цели КПД
|
||
document.getElementById('kpiGoalsForm').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
|
||
appData.kpi.goals = [
|
||
document.getElementById('kpiGoal1').value,
|
||
document.getElementById('kpiGoal2').value,
|
||
document.getElementById('kpiGoal3').value
|
||
];
|
||
saveData(appData);
|
||
|
||
alert('Цели сохранены!');
|
||
});
|
||
|
||
// Заявка в кадровый резерв
|
||
document.getElementById('reserveFormEl').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
|
||
const application = {
|
||
id: Date.now(),
|
||
position: document.getElementById('reservePosition').value,
|
||
experience: document.getElementById('reserveExperience').value,
|
||
motivation: document.getElementById('reserveMotivation').value,
|
||
skills: document.getElementById('reserveSkills').value,
|
||
date: new Date().toISOString().split('T')[0],
|
||
status: 'new'
|
||
};
|
||
|
||
appData.reserve.applications.push(application);
|
||
saveData(appData);
|
||
|
||
document.getElementById('reserveForm').style.display = 'none';
|
||
alert('Заявка подана! HR свяжется с вами.');
|
||
});
|
||
|
||
// Форма автоматизации
|
||
document.getElementById('automationForm').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
|
||
const automation = {
|
||
id: Date.now(),
|
||
process: document.getElementById('autoProcess').value,
|
||
department: document.getElementById('autoDept').value,
|
||
problem: document.getElementById('autoProblem').value,
|
||
solution: document.getElementById('autoSolution').value,
|
||
effect: document.getElementById('autoEffect').value,
|
||
date: new Date().toISOString().split('T')[0],
|
||
status: 'new'
|
||
};
|
||
|
||
appData.automation.push(automation);
|
||
saveData(appData);
|
||
|
||
alert('Предложение по автоматизации отправлено!');
|
||
e.target.reset();
|
||
});
|
||
|
||
// Опросы адаптации
|
||
document.querySelectorAll('[data-survey]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const surveyType = btn.dataset.survey;
|
||
document.getElementById('adaptationSurvey').style.display = 'block';
|
||
document.getElementById('surveyTitle').textContent =
|
||
surveyType === 'week1' ? 'Опрос: Первая неделя' :
|
||
surveyType === 'month1' ? 'Опрос: 1 месяц' : 'Опрос: После испытательного срока';
|
||
|
||
document.querySelectorAll('.survey-rating').forEach(r => r.dataset.value = 0);
|
||
document.querySelectorAll('.rating-star').forEach(s => s.classList.remove('active'));
|
||
document.getElementById('surveyComment').value = '';
|
||
});
|
||
});
|
||
|
||
document.getElementById('adaptationForm').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
|
||
const ratings = document.querySelectorAll('.survey-rating');
|
||
let tasks = 0, support = 0;
|
||
|
||
ratings.forEach(r => {
|
||
if (r.dataset.field === 'tasks') tasks = parseInt(r.dataset.value);
|
||
if (r.dataset.field === 'support') support = parseInt(r.dataset.value);
|
||
});
|
||
|
||
const survey = {
|
||
id: Date.now(),
|
||
type: 'adaptation',
|
||
date: new Date().toISOString().split('T')[0],
|
||
tasks,
|
||
support,
|
||
comment: document.getElementById('surveyComment').value
|
||
};
|
||
|
||
appData.surveys.push(survey);
|
||
saveData(appData);
|
||
|
||
document.getElementById('adaptationSurvey').style.display = 'none';
|
||
alert('Спасибо за участие в опросе!');
|
||
});
|
||
}
|
||
|
||
// Профиль
|
||
function loadProfile() {
|
||
const profile = appData.profile;
|
||
document.getElementById('profileName').textContent = profile.name;
|
||
document.getElementById('userName').textContent = profile.name.split(' ')[0];
|
||
document.getElementById('profileDept').textContent = profile.department;
|
||
document.getElementById('profilePosition').textContent = profile.position;
|
||
document.getElementById('profileTenure').textContent = 'Стаж: ' + profile.tenure;
|
||
|
||
const myReviews = appData.reviews.filter(r => !r.anonymous);
|
||
document.getElementById('statReviews').textContent = myReviews.length;
|
||
document.getElementById('statIdeas').textContent = appData.ideas.filter(i => !i.anonymous && i.author === profile.name).length;
|
||
document.getElementById('statSurveys').textContent = appData.surveys.length;
|
||
|
||
const historyList = document.getElementById('profileHistory');
|
||
historyList.innerHTML = myReviews.slice(0, 5).map(r => `
|
||
<div class="history-item">
|
||
<div class="history-item-header">
|
||
<span class="history-item-title">${getDirectionName(r.direction)}</span>
|
||
<span class="history-item-date">${formatDate(r.date)}</span>
|
||
</div>
|
||
<div class="history-item-status">Статус: ${getStatusName(r.status)}</div>
|
||
</div>
|
||
`).join('') || '<p>Пока нет обращений</p>';
|
||
|
||
// Панель администратора (видна только если isAdmin = true)
|
||
const adminPanel = document.getElementById('adminPanel');
|
||
if (adminPanel && profile.isAdmin) {
|
||
adminPanel.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
// Экспорт всех данных для админа
|
||
function exportAllData() {
|
||
const data = {
|
||
reviews: appData.reviews,
|
||
ideas: appData.ideas,
|
||
motivation: appData.motivation,
|
||
kpi: appData.kpi,
|
||
reserve: appData.reserve
|
||
};
|
||
|
||
const json = JSON.stringify(data, null, 2);
|
||
const blob = new Blob([json], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'pulse_admin_export_' + new Date().toISOString().split('T')[0] + '.json';
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
window.exportAllData = exportAllData;
|
||
|
||
// Аналитика
|
||
function loadAnalytics() {
|
||
const reviews = appData.reviews;
|
||
const ideas = appData.ideas;
|
||
const settings = appData.settings;
|
||
const profile = appData.profile;
|
||
|
||
document.getElementById('akpiEmployees').textContent = settings.totalEmployees;
|
||
document.getElementById('akpiParticipants').textContent = reviews.length;
|
||
document.getElementById('akpiResponse').textContent =
|
||
Math.round((reviews.length / settings.totalEmployees) * 100) + '%';
|
||
|
||
const avgRating = reviews.length > 0
|
||
? (reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length).toFixed(1)
|
||
: '0.0';
|
||
document.getElementById('akpiAvgRating').textContent = avgRating;
|
||
|
||
// Проблемные направления
|
||
const directionCounts = {};
|
||
reviews.forEach(r => {
|
||
directionCounts[r.direction] = (directionCounts[r.direction] || 0) + 1;
|
||
});
|
||
|
||
const sortedDirections = Object.entries(directionCounts)
|
||
.sort((a, b) => b[1] - a[1])
|
||
.slice(0, 5);
|
||
|
||
document.getElementById('problemAreas').innerHTML = sortedDirections.map(([dir, count]) => `
|
||
<div class="problem-item">
|
||
<span class="problem-name">${getDirectionName(dir)}</span>
|
||
<span class="problem-count">${count}</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
// Идеи по категориям
|
||
const categoryCounts = {};
|
||
ideas.forEach(i => {
|
||
categoryCounts[i.category] = (categoryCounts[i.category] || 0) + 1;
|
||
});
|
||
|
||
document.getElementById('ideasByCategory').innerHTML = Object.entries(categoryCounts)
|
||
.sort((a, b) => b[1] - a[1])
|
||
.map(([cat, count]) => `
|
||
<div class="category-item">
|
||
<span class="category-name">${getCategoryName(cat)}</span>
|
||
<span class="category-count">${count}</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
// Показываем админ-панель экспорта только администратору
|
||
const adminExportPanel = document.getElementById('adminExportPanel');
|
||
const exportBtn = document.getElementById('exportBtn');
|
||
const exportBtnLite = document.getElementById('exportBtnLite');
|
||
|
||
if (profile && profile.isAdmin) {
|
||
if (adminExportPanel) adminExportPanel.style.display = 'block';
|
||
if (exportBtn) exportBtn.style.display = 'none'; // Скрываем обычную кнопку
|
||
if (exportBtnLite) exportBtnLite.style.display = 'none';
|
||
} else {
|
||
if (adminExportPanel) adminExportPanel.style.display = 'none';
|
||
if (exportBtn) exportBtn.style.display = 'inline-block';
|
||
if (exportBtnLite) exportBtnLite.style.display = 'inline-block';
|
||
}
|
||
}
|
||
|
||
// Экспорт в Excel для администратора
|
||
function exportToExcel(type) {
|
||
const date = new Date().toISOString().split('T')[0];
|
||
|
||
if (type === 'reviews') {
|
||
const head = ['Дата', 'Подразделение', 'Направление', 'Оценка', 'Комментарий', 'Тип', 'Статус', 'Автор'];
|
||
const body = appData.reviews.map(r => [
|
||
r.date,
|
||
r.department,
|
||
getDirectionName(r.direction),
|
||
r.rating,
|
||
'"' + r.comment.replace(/"/g, '""') + '"',
|
||
r.type,
|
||
r.status,
|
||
r.anonymous ? 'Анонимно' : (r.author || '—')
|
||
].join(';'));
|
||
|
||
const csv = '\uFEFF' + [head.join(';'), ...body].join('\n');
|
||
downloadCSV(csv, `pulse_otzyvy_${date}.csv`);
|
||
}
|
||
|
||
if (type === 'ideas') {
|
||
const head = ['Название', 'Автор', 'Подразделение', 'Категория', 'Проблема', 'Решение', 'Эффект', 'Голоса', 'Статус', 'Дата'];
|
||
const body = appData.ideas.map(i => [
|
||
i.title,
|
||
i.anonymous ? 'Анонимно' : i.author,
|
||
getDeptName(i.department),
|
||
getCategoryName(i.category),
|
||
'"' + i.problem.replace(/"/g, '""') + '"',
|
||
'"' + i.solution.replace(/"/g, '""') + '"',
|
||
'"' + (i.effect || '').replace(/"/g, '""') + '"',
|
||
i.votes,
|
||
getStatusName(i.status),
|
||
i.date
|
||
].join(';'));
|
||
|
||
const csv = '\uFEFF' + [head.join(';'), ...body].join('\n');
|
||
downloadCSV(csv, `pulse idei_${date}.csv`);
|
||
}
|
||
|
||
if (type === 'motivation') {
|
||
const head = ['Тип', 'Кому', 'От кого', 'Причина', 'Дата'];
|
||
const body = appData.motivation.thanks.map(t => [
|
||
getThankTypeName(t.type),
|
||
t.to,
|
||
t.anonymous ? 'Анонимно' : t.from,
|
||
'"' + t.reason.replace(/"/g, '""') + '"',
|
||
t.date
|
||
].join(';'));
|
||
|
||
const csv = '\uFEFF' + [head.join(';'), ...body].join('\n');
|
||
downloadCSV(csv, `pulse_blagodarnosti_${date}.csv`);
|
||
}
|
||
|
||
if (type === 'kpi') {
|
||
const head = ['Компонент', 'Значение %'];
|
||
const body = Object.entries(appData.kpi.components).map(([key, value]) => [
|
||
getKpiComponentName(key),
|
||
value
|
||
].join(';'));
|
||
|
||
body.push(['Общий КПД', appData.kpi.overall]);
|
||
|
||
const csv = '\uFEFF' + [head.join(';'), ...body].join('\n');
|
||
downloadCSV(csv, `pulse_kpd_${date}.csv`);
|
||
}
|
||
|
||
if (type === 'reserve') {
|
||
const head = ['ФИО', 'Подразделение', 'Этап', 'Прогресс %'];
|
||
const body = appData.reserve.participants.map(p => [
|
||
p.name,
|
||
p.dept,
|
||
p.stage,
|
||
p.progress
|
||
].join(';'));
|
||
|
||
const csv = '\uFEFF' + [head.join(';'), ...body].join('\n');
|
||
downloadCSV(csv, `pulse_rezerv_${date}.csv`);
|
||
}
|
||
|
||
if (type === 'all') {
|
||
// Создаём многостраничный Excel через JSON
|
||
const excelData = {
|
||
'Отзывы': appData.reviews.map(r => ({
|
||
'Дата': r.date,
|
||
'Подразделение': r.department,
|
||
'Направление': getDirectionName(r.direction),
|
||
'Оценка': r.rating,
|
||
'Комментарий': r.comment,
|
||
'Тип': r.type,
|
||
'Статус': r.status,
|
||
'Автор': r.anonymous ? 'Анонимно' : (r.author || '—')
|
||
})),
|
||
'Идеи': appData.ideas.map(i => ({
|
||
'Название': i.title,
|
||
'Автор': i.anonymous ? 'Анонимно' : i.author,
|
||
'Подразделение': getDeptName(i.department),
|
||
'Категория': getCategoryName(i.category),
|
||
'Проблема': i.problem,
|
||
'Решение': i.solution,
|
||
'Голоса': i.votes,
|
||
'Статус': getStatusName(i.status),
|
||
'Дата': i.date
|
||
})),
|
||
'Благодарности': appData.motivation.thanks.map(t => ({
|
||
'Кому': t.to,
|
||
'От кого': t.anonymous ? 'Анонимно' : t.from,
|
||
'Причина': t.reason,
|
||
'Тип': getThankTypeName(t.type),
|
||
'Дата': t.date
|
||
})),
|
||
'КПД': [{
|
||
'Общий': appData.kpi.overall,
|
||
'Задачи': appData.kpi.components.tasks,
|
||
'Сроки': appData.kpi.components.deadlines,
|
||
'Качество': appData.kpi.components.quality,
|
||
'Инициатива': appData.kpi.components.initiative,
|
||
'Команда': appData.kpi.components.team
|
||
}],
|
||
'Резерв': appData.reserve.participants.map(p => ({
|
||
'ФИО': p.name,
|
||
'Подразделение': p.dept,
|
||
'Этап': p.stage,
|
||
'Прогресс': p.progress
|
||
}))
|
||
};
|
||
|
||
// Для простоты выгружаем как JSON который можно открыть в Excel
|
||
const json = JSON.stringify(excelData, null, 2);
|
||
const blob = new Blob([json], { type: 'application/json;charset=utf-8;' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `pulse_polnyj_otchet_${date}.json`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
|
||
alert('Полный отчёт скачан в формате JSON. Откройте его в Excel или импортируйте в систему.');
|
||
return;
|
||
}
|
||
|
||
alert('Отчёт скачан!');
|
||
}
|
||
|
||
function downloadCSV(csv, filename) {
|
||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
function getKpiComponentName(key) {
|
||
const names = {
|
||
tasks: 'Выполнение задач',
|
||
deadlines: 'Соблюдение сроков',
|
||
quality: 'Качество работы',
|
||
initiative: 'Инициативность',
|
||
team: 'Командная работа'
|
||
};
|
||
return names[key] || key;
|
||
}
|
||
|
||
window.exportToExcel = exportToExcel;
|
||
window.downloadCSV = downloadCSV;
|
||
|
||
// Модальные окна
|
||
function initModals() {
|
||
document.getElementById('newIdeaBtn').addEventListener('click', () => {
|
||
document.getElementById('ideaModal').classList.add('active');
|
||
});
|
||
}
|
||
|
||
window.closeModal = function(modalId) {
|
||
document.getElementById(modalId).classList.remove('active');
|
||
};
|
||
|
||
// Мотивация
|
||
function loadMotivation() {
|
||
const mot = appData.motivation;
|
||
|
||
if (mot.employeeOfMonth) {
|
||
const empEl = document.getElementById('employeeOfMonth');
|
||
if (empEl) {
|
||
empEl.innerHTML = `
|
||
<div class="badge-avatar">${mot.employeeOfMonth.avatar}</div>
|
||
<div class="badge-name">${mot.employeeOfMonth.name}</div>
|
||
<div class="badge-dept">${mot.employeeOfMonth.dept}</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
if (mot.ideaOfMonth) {
|
||
const ideaEl = document.getElementById('ideaOfMonth');
|
||
if (ideaEl) {
|
||
ideaEl.innerHTML = `
|
||
<div class="idea-badge-title">${mot.ideaOfMonth.title}</div>
|
||
<div class="idea-badge-author">${mot.ideaOfMonth.author}</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
const thanksList = document.getElementById('thanksList');
|
||
if (thanksList && mot.thanks) {
|
||
thanksList.innerHTML = mot.thanks.map(t => `
|
||
<div class="thank-card">
|
||
<div class="thank-header">
|
||
<span class="thank-to">${t.to}</span>
|
||
<span class="thank-type">${getThankTypeName(t.type)}</span>
|
||
</div>
|
||
<div class="thank-reason">${t.reason}</div>
|
||
<div class="thank-meta">
|
||
<span>От: ${t.anonymous ? 'Анонимно' : t.from}</span>
|
||
<span>${formatDate(t.date)}</span>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
}
|
||
|
||
window.showThankForm = function() {
|
||
document.getElementById('thankForm').style.display = 'block';
|
||
document.getElementById('thankFormEl').reset();
|
||
};
|
||
|
||
window.nominateEmployee = function() {
|
||
alert('Форма номинации будет доступна в конце месяца');
|
||
};
|
||
|
||
window.loadAchievements = function() {
|
||
const achievements = appData.motivation.achievements;
|
||
const html = achievements.map(a => `
|
||
<div class="achievement-item">
|
||
<span class="achievement-icon">${a.icon}</span>
|
||
<span class="achievement-name">${a.name}</span>
|
||
<span class="achievement-count">x${a.count}</span>
|
||
</div>
|
||
`).join('');
|
||
alert('Ваши достижения:\n\n' + achievements.map(a => `${a.icon} ${a.name}: ${a.count}`).join('\n'));
|
||
};
|
||
|
||
function getThankTypeName(type) {
|
||
const names = {
|
||
help: 'За помощь',
|
||
mentor: 'За наставничество',
|
||
initiative: 'За инициативу',
|
||
team: 'За команду',
|
||
other: 'Другое'
|
||
};
|
||
return names[type] || type;
|
||
}
|
||
|
||
// КПД
|
||
function loadKPI() {
|
||
const kpi = appData.kpi;
|
||
|
||
document.getElementById('kpiValue').textContent = kpi.overall;
|
||
document.getElementById('kpiCirclePath').style.strokeDasharray = `${kpi.overall}, 100`;
|
||
|
||
const kpiChartCanvas = document.getElementById('kpiChartCanvas');
|
||
if (kpiChartCanvas) {
|
||
const ctx = kpiChartCanvas.getContext('2d');
|
||
drawLineChart(ctx, kpi.history, '#4A90E2', 400, 200);
|
||
}
|
||
}
|
||
|
||
// Кадровый резерв
|
||
function loadReserve() {
|
||
const participants = appData.reserve.participants;
|
||
const list = document.getElementById('participantsList');
|
||
if (list) {
|
||
list.innerHTML = participants.map(p => `
|
||
<div class="participant-card">
|
||
<div class="participant-avatar">👤</div>
|
||
<div class="participant-info">
|
||
<div class="participant-name">${p.name}</div>
|
||
<div class="participant-dept">${p.dept}</div>
|
||
<div class="participant-stage">Этап: ${p.stage}</div>
|
||
</div>
|
||
<div class="participant-progress">
|
||
<div class="progress-bar"><div class="progress-fill" style="width: ${p.progress}%"></div></div>
|
||
<span>${p.progress}%</span>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
}
|
||
|
||
window.showReserveForm = function() {
|
||
document.getElementById('reserveForm').style.display = 'block';
|
||
document.getElementById('reserveForm').scrollIntoView({ behavior: 'smooth' });
|
||
};
|
||
|
||
window.resetFeedbackForm = function() {
|
||
document.getElementById('feedbackForm').reset();
|
||
document.getElementById('feedbackForm').style.display = 'flex';
|
||
document.getElementById('feedbackSuccess').style.display = 'none';
|
||
document.querySelectorAll('.rating-star').forEach(s => s.classList.remove('active'));
|
||
};
|
||
|
||
// Простые графики на Canvas
|
||
function renderCharts() {
|
||
// График вовлечённости (простая имитация)
|
||
const engagementCanvas = document.getElementById('engagementCanvas');
|
||
if (engagementCanvas) {
|
||
const ctx = engagementCanvas.getContext('2d');
|
||
const data = [65, 68, 72, 70, 75, 78];
|
||
drawLineChart(ctx, data, '#4A90E2', 300, 200);
|
||
}
|
||
|
||
// Рейтинг по направлениям
|
||
const ratingCanvas = document.getElementById('ratingCanvas');
|
||
if (ratingCanvas) {
|
||
const ctx = ratingCanvas.getContext('2d');
|
||
const data = [4.2, 3.8, 4.5, 4.0, 3.9];
|
||
drawBarChart(ctx, data, ['#4A90E2', '#5DBCD2', '#00B8CC', '#10B981', '#F59E0B'], 300, 200);
|
||
}
|
||
}
|
||
|
||
function drawLineChart(ctx, data, color, width, height) {
|
||
ctx.canvas.width = width;
|
||
ctx.canvas.height = height;
|
||
ctx.clearRect(0, 0, width, height);
|
||
|
||
const step = width / (data.length - 1);
|
||
const max = Math.max(...data);
|
||
const min = Math.min(...data);
|
||
const range = max - min || 1;
|
||
|
||
ctx.beginPath();
|
||
ctx.strokeStyle = color;
|
||
ctx.lineWidth = 3;
|
||
ctx.lineJoin = 'round';
|
||
|
||
data.forEach((val, i) => {
|
||
const x = i * step;
|
||
const y = height - ((val - min) / range) * (height - 40) - 20;
|
||
if (i === 0) ctx.moveTo(x, y);
|
||
else ctx.lineTo(x, y);
|
||
});
|
||
|
||
ctx.stroke();
|
||
|
||
// Точки
|
||
data.forEach((val, i) => {
|
||
const x = i * step;
|
||
const y = height - ((val - min) / range) * (height - 40) - 20;
|
||
ctx.beginPath();
|
||
ctx.fillStyle = color;
|
||
ctx.arc(x, y, 5, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
});
|
||
}
|
||
|
||
function drawBarChart(ctx, data, colors, width, height) {
|
||
ctx.canvas.width = width;
|
||
ctx.canvas.height = height;
|
||
ctx.clearRect(0, 0, width, height);
|
||
|
||
const barWidth = (width - 40) / data.length - 10;
|
||
const max = Math.max(...data);
|
||
|
||
data.forEach((val, i) => {
|
||
const x = 20 + i * (barWidth + 10);
|
||
const barHeight = (val / max) * (height - 40);
|
||
const y = height - barHeight - 20;
|
||
|
||
ctx.fillStyle = colors[i % colors.length];
|
||
ctx.fillRect(x, y, barWidth, barHeight);
|
||
|
||
// Значение
|
||
ctx.fillStyle = '#333';
|
||
ctx.font = '12px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText(val.toFixed(1), x + barWidth/2, y - 5);
|
||
});
|
||
}
|
||
|
||
// Вспомогательные функции
|
||
function formatDate(dateStr) {
|
||
const d = new Date(dateStr);
|
||
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long' });
|
||
}
|
||
|
||
function getDirectionName(dir) {
|
||
const names = {
|
||
adaptation: 'Адаптация',
|
||
training: 'Обучение',
|
||
discipline: 'Трудовая дисциплина',
|
||
automation: 'Автоматизация',
|
||
innovation: 'Инновации',
|
||
other: 'Другое'
|
||
};
|
||
return names[dir] || dir;
|
||
}
|
||
|
||
function getDeptName(dept) {
|
||
const names = {
|
||
hr: 'HR',
|
||
it: 'IT',
|
||
finance: 'Финансы',
|
||
procurement: 'Закупки',
|
||
sales: 'Продажи',
|
||
support: 'Поддержка',
|
||
other: 'Другое'
|
||
};
|
||
return names[dept] || dept;
|
||
}
|
||
|
||
function getStatusName(status) {
|
||
const names = {
|
||
new: 'Новое',
|
||
review: 'На рассмотрении',
|
||
accepted: 'Принято',
|
||
implement: 'В работе',
|
||
done: 'Решено',
|
||
rejected: 'Отклонено'
|
||
};
|
||
return names[status] || status;
|
||
}
|
||
|
||
function getCategoryName(cat) {
|
||
const names = {
|
||
process: 'Оптимизация процессов',
|
||
automation: 'Автоматизация',
|
||
cost: 'Снижение затрат',
|
||
quality: 'Повышение качества',
|
||
safety: 'Безопасность',
|
||
other: 'Другое'
|
||
};
|
||
return names[cat] || cat;
|
||
}
|