565 lines
18 KiB
JavaScript
565 lines
18 KiB
JavaScript
// 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 `
|
||
<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('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>';
|
||
}
|
||
|
||
// Аналитика
|
||
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]) => `
|
||
<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('');
|
||
}
|
||
|
||
// Экспорт в 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;
|
||
}
|