// Pulse App - Main JavaScript
// Инициализация при загрузке
document.addEventListener('DOMContentLoaded', () => {
initNavigation();
initRatingInputs();
loadDashboard();
loadIdeas();
loadProfile();
initForms();
initFilters();
initModals();
renderCharts();
});
// Навигация между секциями
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 `
👤 ${idea.anonymous ? 'Анонимно' : idea.author}
📅 ${formatDate(idea.date)}
🏢 ${getDeptName(idea.department)}
Проблема: ${idea.problem}
Решение: ${idea.solution}
`;
}
// Загрузка идей
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('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 => `
Статус: ${getStatusName(r.status)}
`).join('') || 'Пока нет обращений
';
}
// Аналитика
function loadAnalytics() {
const reviews = appData.reviews;
const ideas = appData.ideas;
const settings = appData.settings;
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]) => `
${getDirectionName(dir)}
${count}
`).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]) => `
${getCategoryName(cat)}
${count}
`).join('');
}
// Экспорт в Excel
document.getElementById('exportBtn')?.addEventListener('click', () => {
// Создаём CSV с BOM для Excel
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
].join(';'));
const csv = '\uFEFF' + [head.join(';'), ...body].join('\n');
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 = 'pulse_feedback_' + new Date().toISOString().split('T')[0] + '.csv';
a.click();
URL.revokeObjectURL(url);
});
// Модальные окна
function initModals() {
document.getElementById('newIdeaBtn').addEventListener('click', () => {
document.getElementById('ideaModal').classList.add('active');
});
}
window.closeModal = function(modalId) {
document.getElementById(modalId).classList.remove('active');
};
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;
}