// 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 `
${idea.title}
${statusLabels[idea.status]}
👤 ${idea.anonymous ? 'Анонимно' : idea.author} 📅 ${formatDate(idea.date)} 🏢 ${getDeptName(idea.department)}
Проблема: ${idea.problem}
Решение: ${idea.solution}
💬 ${idea.comments} комментариев
`; } // Загрузка идей 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 => `
${getDirectionName(r.direction)} ${formatDate(r.date)}
Статус: ${getStatusName(r.status)}
`).join('') || '

Пока нет обращений

'; // Панель администратора (видна только если 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]) => `
${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(''); // Показываем админ-панель экспорта только администратору 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 = `
${mot.employeeOfMonth.avatar}
${mot.employeeOfMonth.name}
${mot.employeeOfMonth.dept}
`; } } if (mot.ideaOfMonth) { const ideaEl = document.getElementById('ideaOfMonth'); if (ideaEl) { ideaEl.innerHTML = `
${mot.ideaOfMonth.title}
${mot.ideaOfMonth.author}
`; } } const thanksList = document.getElementById('thanksList'); if (thanksList && mot.thanks) { thanksList.innerHTML = mot.thanks.map(t => `
${t.to} ${getThankTypeName(t.type)}
${t.reason}
От: ${t.anonymous ? 'Анонимно' : t.from} ${formatDate(t.date)}
`).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 => `
${a.icon} ${a.name} x${a.count}
`).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 => `
👤
${p.name}
${p.dept}
Этап: ${p.stage}
${p.progress}%
`).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; }