/
lenaz21
/
Chek
Обзор
Документация
Войти
/
lenaz21
/
Chek
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
index.html
1 014 строк
34 KB
lenaz21
Create: index.html
30 июл 2026, 09:49
Верифицирован
30 июл 2026, 09:49
5f6f24e
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Чек-лист оценки звонка</title> <!-- Подключаем Chart.js для графиков --> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f4f7fc; padding: 30px 20px; color: #1e293b; } .container { max-width: 1200px; margin: 0 auto; background: #ffffff; border-radius: 24px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.08); overflow: hidden; } /* --- Вкладки --- */ .tabs { display: flex; border-bottom: 2px solid #e9edf4; background: #fafbfd; padding: 0 24px; } .tab-btn { padding: 18px 32px; font-size: 16px; font-weight: 600; border: none; background: transparent; color: #64748b; cursor: pointer; border-bottom: 3px solid transparent; transition: all 0.2s; letter-spacing: 0.3px; } .tab-btn:hover { color: #1e293b; background: #f1f5f9; } .tab-btn.active { color: #2563eb; border-bottom-color: #2563eb; background: transparent; } .tab-content { display: none; padding: 32px 32px 40px; animation: fadeIn 0.3s ease; } .tab-content.active { display: block; } @keyframes fadeIn { from { opacity: 0.5; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } /* --- Форма --- */ .form-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px; flex-wrap: wrap; gap: 12px; } .form-header h2 { font-size: 24px; font-weight: 700; } .badge { background: #e9edf4; padding: 6px 16px; border-radius: 40px; font-size: 14px; color: #475569; } .criteria-list { display: flex; flex-direction: column; gap: 22px; margin-bottom: 32px; } .criteria-item { background: #f8fafc; border-radius: 16px; padding: 16px 22px; display: flex; flex-wrap: wrap; align-items: center; gap: 16px 24px; border: 1px solid #eef2f6; transition: 0.15s; } .criteria-item:hover { background: #f1f5f9; border-color: #d1d9e6; } .criteria-label { flex: 1 1 240px; font-weight: 600; font-size: 16px; color: #0f172a; } .criteria-label small { font-weight: 400; color: #64748b; font-size: 13px; display: block; margin-top: 2px; } .rating-group { display: flex; gap: 6px; flex-wrap: wrap; } .rating-group label { display: flex; align-items: center; gap: 3px; font-size: 14px; font-weight: 500; color: #334155; cursor: pointer; padding: 4px 10px; border-radius: 30px; background: #ffffff; border: 1px solid #dce2ec; transition: 0.15s; } .rating-group label:hover { background: #e9edf4; } .rating-group input[type="radio"] { accent-color: #2563eb; width: 16px; height: 16px; margin: 0; cursor: pointer; } .rating-group label:has(input:checked) { background: #dbeafe; border-color: #2563eb; color: #1e3a8a; } .form-actions { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 8px; } .btn { padding: 12px 32px; border: none; border-radius: 40px; font-weight: 600; font-size: 15px; cursor: pointer; transition: 0.15s; display: inline-flex; align-items: center; gap: 8px; } .btn-primary { background: #2563eb; color: white; box-shadow: 0 4px 12px rgba(37, 99, 235, 0.25); } .btn-primary:hover { background: #1d4ed8; transform: translateY(-2px); box-shadow: 0 8px 20px rgba(37, 99, 235, 0.3); } .btn-secondary { background: #e9edf4; color: #1e293b; } .btn-secondary:hover { background: #d1d9e6; } .btn-success { background: #16a34a; color: white; } .btn-success:hover { background: #15803d; } .btn-danger { background: #dc2626; color: white; } .btn-danger:hover { background: #b91c1c; } .toast { margin-top: 20px; padding: 14px 22px; border-radius: 12px; background: #dcfce7; color: #166534; border-left: 5px solid #22c55e; font-weight: 500; display: none; } .toast.show { display: block; } /* --- Статистика --- */ .stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-top: 10px; } .stats-card { background: #f8fafc; border-radius: 16px; padding: 20px 22px 24px; border: 1px solid #eef2f6; } .stats-card h3 { font-size: 18px; font-weight: 600; margin-bottom: 14px; color: #0f172a; } .chart-container { position: relative; height: 300px; width: 100%; } .stats-numbers { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 18px; } .stat-item { background: white; border-radius: 12px; padding: 12px 16px; border: 1px solid #e9edf4; } .stat-item .label { font-size: 13px; color: #64748b; } .stat-item .value { font-size: 22px; font-weight: 700; color: #0f172a; } .stat-item .value.good { color: #16a34a; } .stat-item .value.medium { color: #eab308; } .stat-item .value.bad { color: #dc2626; } .empty-state { text-align: center; padding: 40px 20px; color: #64748b; } .empty-state svg { opacity: 0.4; margin-bottom: 12px; } /* --- Таблица истории --- */ .history-table-wrapper { margin-top: 30px; overflow-x: auto; border-radius: 12px; border: 1px solid #eef2f6; } .history-table { width: 100%; border-collapse: collapse; font-size: 14px; min-width: 700px; } .history-table thead { background: #f1f5f9; } .history-table th { padding: 12px 16px; text-align: left; font-weight: 600; color: #1e293b; border-bottom: 2px solid #dce2ec; white-space: nowrap; } .history-table td { padding: 10px 16px; border-bottom: 1px solid #eef2f6; color: #334155; } .history-table tbody tr:hover { background: #f8fafc; } .history-table .rating-cell { text-align: center; font-weight: 600; border-radius: 6px; padding: 4px 8px; display: inline-block; min-width: 30px; } .rating-cell.good { background: #dcfce7; color: #16a34a; } .rating-cell.medium { background: #fef9c3; color: #ca8a04; } .rating-cell.bad { background: #fee2e2; color: #dc2626; } .history-table .avg-cell { font-weight: 700; text-align: center; } /* Адаптив */ @media (max-width: 768px) { .stats-grid { grid-template-columns: 1fr; } .tabs { overflow-x: auto; padding: 0 12px; } .tab-btn { padding: 14px 18px; font-size: 14px; white-space: nowrap; } .tab-content { padding: 20px 16px 30px; } .criteria-item { padding: 14px 16px; } .rating-group label { padding: 2px 8px; font-size: 13px; } .chart-container { height: 260px; } .history-table { font-size: 12px; min-width: 500px; } .history-table th, .history-table td { padding: 8px 10px; } } </style> </head> <body> <div class="container"> <!-- Вкладки --> <div class="tabs"> <button class="tab-btn active" data-tab="tab-form">📋 Оценка звонка</button> <button class="tab-btn" data-tab="tab-stats">📊 Статистика</button> </div> <!-- ===================== ВКЛАДКА 1: ФОРМА ===================== --> <div id="tab-form" class="tab-content active"> <div class="form-header"> <h2>📞 Чек-лист оценки звонка</h2> <span class="badge" id="totalCountForm">Всего оценок: 0</span> </div> <form id="evaluationForm"> <div class="criteria-list" id="criteriaList"> <!-- Критерии будут добавлены через JS --> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary">💾 Сохранить оценку</button> <button type="button" class="btn btn-secondary" id="clearFormBtn">🔄 Очистить форму</button> </div> </form> <div id="toastMessage" class="toast">✅ Оценка сохранена!</div> </div> <!-- ===================== ВКЛАДКА 2: СТАТИСТИКА ===================== --> <div id="tab-stats" class="tab-content"> <div class="form-header"> <h2>📊 Общая статистика по ответам</h2> <span class="badge" id="totalCountStats">Всего оценок: 0</span> </div> <div id="statsContainer"> <div class="empty-state"> <p style="font-size: 18px; font-weight: 500;">Нет данных</p> <p style="color: #94a3b8;">Сохраните хотя бы одну оценку, чтобы увидеть статистику</p> </div> </div> </div> </div> <script> (function() { 'use strict'; // ---------- КОНФИГ ---------- const CRITERIA = [ { id: 'comfort', label: 'Комфортность общения', desc: 'голос, темп речи, интонация' }, { id: 'active_listening', label: 'Техника активного слушания', desc: 'поддакивание, парафраз, уточнения' }, { id: 'business_style', label: 'Соблюдение норм делового стиля', desc: 'лексика, грамотность, тон' }, { id: 'info_accuracy', label: 'Корректность предоставленной информации', desc: 'факты, цифры, детали' }, { id: 'empathy', label: 'Вовлеченность и сопереживание', desc: 'эмпатия, внимание к Пользователю / Заявителю' } ]; // Короткие названия для диаграмм const SHORT_LABELS = [ 'Комфортность', 'Активное слушание', 'Деловой стиль', 'Корректность инфо', 'Сопереживание' ]; const STORAGE_KEY = 'callEvaluationData'; // ---------- СОСТОЯНИЕ ---------- let allRecords = []; // Ссылки на DOM const criteriaListEl = document.getElementById('criteriaList'); const formEl = document.getElementById('evaluationForm'); const toastEl = document.getElementById('toastMessage'); const clearFormBtn = document.getElementById('clearFormBtn'); const totalCountForm = document.getElementById('totalCountForm'); const totalCountStats = document.getElementById('totalCountStats'); const statsContainer = document.getElementById('statsContainer'); // ---------- ЗАГРУЗКА ДАННЫХ ---------- function loadData() { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { allRecords = JSON.parse(raw); if (!Array.isArray(allRecords)) allRecords = []; } else { allRecords = []; } } catch (e) { allRecords = []; } allRecords = allRecords.filter(rec => { return CRITERIA.every(c => typeof rec[c.id] === 'number' && rec[c.id] >= 1 && rec[c.id] <= 5); }); } function saveData() { localStorage.setItem(STORAGE_KEY, JSON.stringify(allRecords)); } // ---------- ОТРИСОВКА ФОРМЫ ---------- function renderForm() { criteriaListEl.innerHTML = ''; CRITERIA.forEach((c, index) => { const div = document.createElement('div'); div.className = 'criteria-item'; const labelDiv = document.createElement('div'); labelDiv.className = 'criteria-label'; labelDiv.innerHTML = `${c.label} <small>${c.desc}</small>`; const ratingDiv = document.createElement('div'); ratingDiv.className = 'rating-group'; for (let val = 1; val <= 5; val++) { const radioId = `crit_${c.id}_${val}`; const label = document.createElement('label'); label.htmlFor = radioId; const input = document.createElement('input'); input.type = 'radio'; input.name = c.id; input.id = radioId; input.value = val; input.required = true; label.appendChild(input); label.appendChild(document.createTextNode(val)); ratingDiv.appendChild(label); } div.appendChild(labelDiv); div.appendChild(ratingDiv); criteriaListEl.appendChild(div); }); } // ---------- СБОР ДАННЫХ ИЗ ФОРМЫ ---------- function getFormData() { const data = {}; let valid = true; CRITERIA.forEach(c => { const selected = document.querySelector(`input[name="${c.id}"]:checked`); if (selected) { data[c.id] = parseInt(selected.value, 10); } else { valid = false; data[c.id] = null; } }); return { data, valid }; } function resetForm() { document.querySelectorAll('#criteriaList input[type="radio"]').forEach(el => el.checked = false); } function showToast(message, isError = false) { toastEl.textContent = message; toastEl.style.display = 'block'; if (isError) { toastEl.style.background = '#fee2e2'; toastEl.style.color = '#991b1b'; toastEl.style.borderLeftColor = '#dc2626'; } else { toastEl.style.background = '#dcfce7'; toastEl.style.color = '#166534'; toastEl.style.borderLeftColor = '#22c55e'; } setTimeout(() => { toastEl.style.display = 'none'; }, 3000); } // ---------- ДОБАВЛЕНИЕ ОЦЕНКИ ---------- function addRecord(formData) { const record = { timestamp: new Date().toLocaleString('ru-RU') }; CRITERIA.forEach(c => { record[c.id] = formData[c.id]; }); allRecords.push(record); saveData(); updateCounters(); renderStatistics(); resetForm(); showToast('✅ Оценка сохранена!'); } // ---------- ОБНОВЛЕНИЕ СЧЕТЧИКОВ ---------- function updateCounters() { const count = allRecords.length; totalCountForm.textContent = `Всего оценок: ${count}`; totalCountStats.textContent = `Всего оценок: ${count}`; } // ---------- СТАТИСТИКА: ВЫЧИСЛЕНИЯ ---------- function computeStats() { if (allRecords.length === 0) return null; const sums = {}; const counts = {}; CRITERIA.forEach(c => { sums[c.id] = 0; counts[c.id] = 0; }); allRecords.forEach(rec => { CRITERIA.forEach(c => { const val = rec[c.id]; if (typeof val === 'number' && val >= 1 && val <= 5) { sums[c.id] += val; counts[c.id] += 1; } }); }); const averages = {}; CRITERIA.forEach(c => { averages[c.id] = counts[c.id] > 0 ? sums[c.id] / counts[c.id] : 0; }); const totalAvg = CRITERIA.reduce((acc, c) => acc + averages[c.id], 0) / CRITERIA.length; return { averages, totalAvg, count: allRecords.length, distribution: computeDistribution() }; } function computeDistribution() { const dist = {}; CRITERIA.forEach(c => { dist[c.id] = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; }); allRecords.forEach(rec => { CRITERIA.forEach(c => { const val = rec[c.id]; if (typeof val === 'number' && val >= 1 && val <= 5) { dist[c.id][val] = (dist[c.id][val] || 0) + 1; } }); }); return dist; } // ---------- ОТРИСОВКА СТАТИСТИКИ (с графиками и таблицей) ---------- let pieChartInstance = null; let barChartInstance = null; function renderStatistics() { const stats = computeStats(); if (!stats || allRecords.length === 0) { statsContainer.innerHTML = ` <div class="empty-state"> <p style="font-size: 18px; font-weight: 500;">Нет данных</p> <p style="color: #94a3b8;">Сохраните хотя бы одну оценку, чтобы увидеть статистику</p> </div> `; return; } const avgValues = CRITERIA.map(c => Math.round(stats.averages[c.id] * 100) / 100); // Цветовая палитра для круговой диаграммы const colors = [ '#2563eb', // синий '#22c55e', // зеленый '#eab308', // желтый '#8b5cf6', // фиолетовый '#ec4899' // розовый ]; // --- Строим таблицу истории --- let tableRows = ''; allRecords.forEach((rec, index) => { const values = CRITERIA.map(c => rec[c.id]); const avg = (values.reduce((a, b) => a + b, 0) / values.length).toFixed(2); const avgNum = parseFloat(avg); let avgClass = 'medium'; if (avgNum >= 4) avgClass = 'good'; else if (avgNum < 3) avgClass = 'bad'; tableRows += ` <tr> <td>${rec.timestamp || '—'}</td> ${values.map(v => { let cls = 'medium'; if (v >= 4) cls = 'good'; else if (v < 3) cls = 'bad'; return `<td><span class="rating-cell ${cls}">${v}</span></td>`; }).join('')} <td class="avg-cell"><span class="rating-cell ${avgClass}">${avg}</span></td> </tr> `; }); const tableHtml = ` <div class="stats-card" style="margin-top: 24px;"> <h3>📋 История всех оценок</h3> <div class="history-table-wrapper"> <table class="history-table"> <thead> <tr> <th>📅 Дата/время</th> ${CRITERIA.map(c => `<th>${SHORT_LABELS[CRITERIA.indexOf(c)]}</th>`).join('')} <th>⭐ Средний</th> </tr> </thead> <tbody> ${tableRows} </tbody> </table> </div> <div style="margin-top: 12px; font-size: 13px; color: #64748b;"> <span class="rating-cell good" style="display:inline-block; padding:2px 10px; margin-right:8px;">4-5</span> — высокий <span class="rating-cell medium" style="display:inline-block; padding:2px 10px; margin:0 8px;">3</span> — средний <span class="rating-cell bad" style="display:inline-block; padding:2px 10px; margin-left:8px;">1-2</span> — низкий </div> </div> `; // --- Основная статистика --- let html = ` <div style="margin-bottom: 16px; display: flex; gap: 12px; flex-wrap: wrap;"> <span class="badge" style="background:#2563eb; color:white; font-size:15px; padding:6px 20px;"> 📌 Общий средний балл: ${stats.totalAvg.toFixed(2)} </span> <span class="badge" style="background:#e9edf4; font-size:15px; padding:6px 20px;"> 📝 Всего оценок: ${stats.count} </span> </div> <div class="stats-grid"> <div class="stats-card"> <h3>🍩 Круговая диаграмма (распределение средних баллов)</h3> <div class="chart-container"> <canvas id="pieChart"></canvas> </div> </div> <div class="stats-card"> <h3>📊 Столбчатая диаграмма (средние баллы)</h3> <div class="chart-container"> <canvas id="barChart"></canvas> </div> </div> </div> <div class="stats-card" style="margin-top: 24px;"> <h3>📋 Детальные средние по критериям</h3> <div class="stats-numbers"> ${CRITERIA.map((c, idx) => { const avg = stats.averages[c.id]; let cls = 'medium'; if (avg >= 4) cls = 'good'; else if (avg < 3) cls = 'bad'; return ` <div class="stat-item"> <div class="label">${c.label}</div> <div class="value ${cls}" style="color: ${colors[idx]};">${avg.toFixed(2)}</div> </div> `; }).join('')} </div> </div> ${tableHtml} `; statsContainer.innerHTML = html; // ---------- РИСУЕМ ГРАФИКИ ---------- const labelsForChart = SHORT_LABELS; // КРУГОВАЯ ДИАГРАММА const pieCtx = document.getElementById('pieChart').getContext('2d'); if (pieChartInstance) pieChartInstance.destroy(); pieChartInstance = new Chart(pieCtx, { type: 'doughnut', data: { labels: labelsForChart, datasets: [{ data: avgValues, backgroundColor: colors, borderColor: '#ffffff', borderWidth: 3, hoverOffset: 10 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { font: { size: 11, weight: '600' }, padding: 10, usePointStyle: true, pointStyle: 'circle', boxWidth: 12, boxHeight: 12 } }, tooltip: { callbacks: { label: function(context) { const total = context.dataset.data.reduce((a, b) => a + b, 0); const percentage = total > 0 ? ((context.parsed / total) * 100).toFixed(1) : 0; return `${context.label}: ${context.parsed.toFixed(2)} баллов (${percentage}%)`; } } } } }, plugins: [{ id: 'customLabels', afterDraw(chart) { const { ctx, data, chartArea: { top, bottom, left, right } } = chart; const centerX = (left + right) / 2; const centerY = (top + bottom) / 2; const radius = Math.min(right - left, bottom - top) / 2 * 0.65; const dataset = data.datasets[0]; const total = dataset.data.reduce((a, b) => a + b, 0); if (total === 0) return; let currentAngle = -Math.PI / 2; const arc = (2 * Math.PI) / dataset.data.length; dataset.data.forEach((value, index) => { const percentage = (value / total) * 100; if (percentage < 3) return; const angle = currentAngle + arc / 2; const x = centerX + Math.cos(angle) * radius; const y = centerY + Math.sin(angle) * radius; ctx.save(); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.font = 'bold 12px Segoe UI, sans-serif'; ctx.fillStyle = '#ffffff'; ctx.shadowColor = 'rgba(0,0,0,0.3)'; ctx.shadowBlur = 6; ctx.fillText(`${percentage.toFixed(1)}%`, x, y); ctx.restore(); currentAngle += arc; }); } }] }); // Столбчатая диаграмма const barCtx = document.getElementById('barChart').getContext('2d'); if (barChartInstance) barChartInstance.destroy(); barChartInstance = new Chart(barCtx, { type: 'bar', data: { labels: labelsForChart, datasets: [{ label: 'Средний балл', data: avgValues, backgroundColor: avgValues.map(v => { if (v >= 4) return '#22c55e'; if (v >= 3) return '#eab308'; return '#ef4444'; }), borderRadius: 6, borderSkipped: false, }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { min: 0, max: 5, ticks: { stepSize: 1, font: { size: 11 } }, grid: { color: '#e9edf4' } }, x: { grid: { display: false }, ticks: { font: { size: 10, weight: '500' }, maxRotation: 0 } } }, plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(context) { return `Средний балл: ${context.parsed.y.toFixed(2)}`; } } } } } }); } // ---------- ПЕРЕКЛЮЧЕНИЕ ВКЛАДОК ---------- function initTabs() { const buttons = document.querySelectorAll('.tab-btn'); const contents = { 'tab-form': document.getElementById('tab-form'), 'tab-stats': document.getElementById('tab-stats') }; buttons.forEach(btn => { btn.addEventListener('click', function() { const tabId = this.dataset.tab; buttons.forEach(b => b.classList.remove('active')); this.classList.add('active'); Object.keys(contents).forEach(key => { contents[key].classList.toggle('active', key === tabId); }); if (tabId === 'tab-stats') { renderStatistics(); } }); }); } // ---------- ОЧИСТКА ВСЕХ ДАННЫХ ---------- function clearAllData() { if (allRecords.length === 0) { showToast('Нет данных для удаления', true); return; } if (confirm('Вы уверены, что хотите удалить ВСЕ сохраненные оценки?')) { allRecords = []; saveData(); updateCounters(); renderStatistics(); resetForm(); showToast('🗑️ Все данные удалены'); } } // ---------- ИНИЦИАЛИЗАЦИЯ ---------- function init() { loadData(); renderForm(); updateCounters(); renderStatistics(); formEl.addEventListener('submit', function(e) { e.preventDefault(); const { data, valid } = getFormData(); if (!valid) { showToast('⚠️ Пожалуйста, оцените все 5 критериев', true); return; } addRecord(data); }); clearFormBtn.addEventListener('click', function() { resetForm(); showToast('Форма очищена', false); }); const actionsDiv = document.querySelector('.form-actions'); const clearAllBtn = document.createElement('button'); clearAllBtn.type = 'button'; clearAllBtn.className = 'btn btn-danger'; clearAllBtn.textContent = '🗑️ Удалить все данные'; clearAllBtn.addEventListener('click', clearAllData); actionsDiv.appendChild(clearAllBtn); initTabs(); } document.addEventListener('DOMContentLoaded', init); })(); </script> </body> </html>