tehnicheskoe-zadanie-na-razr/app.js

565 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
}