treker-obucheniya/index.html

940 lines
42 KiB
HTML
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.

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Трекер обучения сотрудников</title>
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<style>
:root {
--primary: #2563eb;
--primary-dark: #1d4ed8;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--bg: #f8fafc;
--card-bg: #ffffff;
--text: #1e293b;
--text-muted: #64748b;
--border: #e2e8f0;
--shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05);
}
* { margin: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); line-height: 1.5; }
.container { max-width: 1400px; margin: 0 auto; padding: 24px; }
header { background: var(--card-bg); border-bottom: 1px solid var(--border); padding: 20px 0; margin-bottom: 24px; }
header .container { display: flex; justify-content: space-between; align-items: center; }
h1 { font-size: 28px; font-weight: 700; color: var(--text); }
.subtitle { color: var(--text-muted); font-size: 14px; margin-top: 4px; }
.btn { display: inline-flex; align-items: center; gap: 8px; padding: 10px 16px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover { background: var(--primary-dark); }
.btn-secondary { background: var(--bg); color: var(--text); border: 1px solid var(--border); }
.btn-secondary:hover { background: var(--border); }
.btn-success { background: var(--success); color: white; }
.btn-danger { background: var(--danger); color: white; }
.btn-sm { padding: 6px 12px; font-size: 13px; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
.card { background: var(--card-bg); border-radius: 12px; padding: 20px; box-shadow: var(--shadow); }
.card h3 { font-size: 14px; color: var(--text-muted); font-weight: 500; margin-bottom: 8px; }
.card .value { font-size: 32px; font-weight: 700; color: var(--text); }
.card .delta { font-size: 13px; margin-top: 8px; }
.card .delta.up { color: var(--success); }
.card .delta.down { color: var(--danger); }
.section { background: var(--card-bg); border-radius: 12px; padding: 20px; margin-bottom: 24px; box-shadow: var(--shadow); }
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.section h2 { font-size: 18px; font-weight: 600; }
.filters { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.filters select, .filters input { padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; background: white; }
.filters select:focus, .filters input:focus { outline: 2px solid var(--primary); border-color: transparent; }
.table-container { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid var(--border); }
th { font-size: 13px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; background: var(--bg); }
td { font-size: 14px; }
tr:hover { background: var(--bg); }
.status { display: inline-block; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 500; }
.status-pending { background: #fef3c7; color: #92400e; }
.status-progress { background: #dbeafe; color: #1e40af; }
.status-completed { background: #d1fae5; color: #065f46; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.dept-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; }
.dept-card { background: var(--bg); border-radius: 8px; padding: 16px; }
.dept-card h4 { font-size: 15px; margin-bottom: 12px; }
.dept-row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 13px; border-bottom: 1px solid var(--border); }
.dept-row:last-child { border-bottom: none; }
.tabs { display: flex; gap: 4px; margin-bottom: 20px; border-bottom: 2px solid var(--border); }
.tab { padding: 12px 20px; cursor: pointer; font-weight: 500; color: var(--text-muted); border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.2s; }
.tab:hover { color: var(--text); }
.tab.active { color: var(--primary); border-bottom-color: var(--primary); }
.hidden { display: none; }
/* Modal */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal { background: var(--card-bg); border-radius: 12px; padding: 24px; width: 100%; max-width: 500px; max-height: 90vh; overflow-y: auto; }
.modal h3 { font-size: 20px; margin-bottom: 20px; }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; font-size: 14px; font-weight: 500; margin-bottom: 6px; }
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; }
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: 2px solid var(--primary); border-color: transparent; }
.form-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px; }
.courses-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
.course-card { background: var(--bg); border-radius: 8px; padding: 16px; cursor: pointer; transition: transform 0.2s; }
.course-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); }
.course-card h4 { font-size: 16px; margin-bottom: 8px; }
.course-card .meta { font-size: 13px; color: var(--text-muted); margin-bottom: 12px; }
.course-stats { display: flex; gap: 16px; font-size: 13px; }
.course-stat { display: flex; flex-direction: column; }
.course-stat .num { font-weight: 700; font-size: 18px; }
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; font-size: 14px; }
.alert-info { background: #dbeafe; color: #1e40af; }
.alert-success { background: #d1fae5; color: #065f46; }
.alert-warning { background: #fef3c7; color: #92400e; }
.progress-bar { height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; margin-top: 8px; }
.progress-fill { height: 100%; background: var(--primary); transition: width 0.3s; }
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
.empty-state h3 { font-size: 18px; margin-bottom: 8px; color: var(--text); }
@media (max-width: 768px) {
.container { padding: 16px; }
header .container { flex-direction: column; gap: 16px; text-align: center; }
.filters { flex-direction: column; }
.filters select, .filters input { width: 100%; }
.cards { grid-template-columns: repeat(2, 1fr); }
.dept-stats { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<header>
<div class="container">
<div>
<h1>📚 Трекер обучения</h1>
<p class="subtitle">Управление курсами и контроль прохождения сотрудниками</p>
</div>
<div style="display: flex; gap: 12px;">
<button class="btn btn-secondary" onclick="openImportModal()">📥 Импорт</button>
<button class="btn btn-secondary" onclick="exportToExcel()">📊 Экспорт Excel</button>
<button class="btn btn-primary" onclick="openRecordModal()">+ Запись</button>
</div>
</div>
</header>
<div class="container">
<!-- Сводные карточки -->
<div class="cards">
<div class="card">
<h3>Всего курсов</h3>
<div class="value" id="statCourses">0</div>
<div class="delta" id="statCoursesDelta"></div>
</div>
<div class="card">
<h3>Записей на обучение</h3>
<div class="value" id="statRecords">0</div>
<div class="delta" id="statRecordsDelta"></div>
</div>
<div class="card">
<h3>Пройдено</h3>
<div class="value" id="statCompleted">0</div>
<div class="delta up" id="statCompletedDelta"></div>
<div class="progress-bar"><div class="progress-fill" id="progressCompleted" style="width: 0%"></div></div>
</div>
<div class="card">
<h3>Просрочено</h3>
<div class="value" id="statOverdue">0</div>
<div class="delta down" id="statOverdueDelta"></div>
</div>
</div>
<!-- Вкладки -->
<div class="tabs">
<div class="tab active" onclick="switchTab('records')">Записи</div>
<div class="tab" onclick="switchTab('courses')">Курсы</div>
<div class="tab" onclick="switchTab('departments')">Сводка по отделам</div>
</div>
<!-- Вкладка: Записи -->
<div id="tab-records" class="section">
<div class="section-header">
<h2>Все записи на обучение</h2>
<button class="btn btn-primary btn-sm" onclick="openRecordModal()">+ Добавить запись</button>
</div>
<div class="filters">
<input type="text" id="filterSearch" placeholder="🔍 Поиск сотрудника..." oninput="applyFilters()">
<select id="filterDept" onchange="applyFilters()">
<option value="">Все отделы</option>
</select>
<select id="filterCourse" onchange="applyFilters()">
<option value="">Все курсы</option>
</select>
<select id="filterStatus" onchange="applyFilters()">
<option value="">Все статусы</option>
<option value="pending">Запланировано</option>
<option value="progress">В процессе</option>
<option value="completed">Пройдено</option>
<option value="overdue">Просрочено</option>
</select>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Сотрудник</th>
<th>Отдел</th>
<th>Курс</th>
<th>Дедлайн</th>
<th>Статус</th>
<th>Дата прохождения</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="recordsTable"></tbody>
</table>
</div>
<div id="recordsEmpty" class="empty-state hidden">
<h3>Нет записей</h3>
<p>Добавьте первую запись на обучение</p>
</div>
</div>
<!-- Вкладка: Курсы -->
<div id="tab-courses" class="section hidden">
<div class="section-header">
<h2>Каталог курсов</h2>
<button class="btn btn-primary btn-sm" onclick="openCourseModal()">+ Добавить курс</button>
</div>
<div class="courses-grid" id="coursesGrid"></div>
<div id="coursesEmpty" class="empty-state hidden">
<h3>Нет курсов</h3>
<p>Добавьте первый курс обучения</p>
</div>
</div>
<!-- Вкладка: Сводка по отделам -->
<div id="tab-departments" class="section hidden">
<div class="section-header">
<h2>Статистика по отделам</h2>
</div>
<div class="dept-stats" id="deptStats"></div>
</div>
</div>
<!-- Модальное окно: Запись -->
<div id="recordModal" class="modal-overlay hidden">
<div class="modal">
<h3 id="recordModalTitle">Новая запись на обучение</h3>
<form id="recordForm" onsubmit="saveRecord(event)">
<input type="hidden" id="recordId">
<div class="form-group">
<label>Сотрудник *</label>
<input type="text" id="recordEmployee" required placeholder="ФИО сотрудника">
</div>
<div class="form-group">
<label>Отдел *</label>
<select id="recordDept" required>
<option value="">Выберите отдел</option>
<option value="IT">IT</option>
<option value="HR">HR</option>
<option value="Продажи">Продажи</option>
<option value="Маркетинг">Маркетинг</option>
<option value="Финансы">Финансы</option>
<option value="Юридический">Юридический</option>
<option value="Операционный">Операционный</option>
<option value="Безопасность">Безопасность</option>
</select>
</div>
<div class="form-group">
<label>Курс *</label>
<select id="recordCourse" required>
<option value="">Выберите курс</option>
</select>
</div>
<div class="form-group">
<label>Статус *</label>
<select id="recordStatus" required onchange="toggleCompletionDate()">
<option value="pending">Запланировано</option>
<option value="progress">В процессе</option>
<option value="completed">Пройдено</option>
</select>
</div>
<div class="form-group">
<label>Дедлайн *</label>
<input type="date" id="recordDeadline" required>
</div>
<div class="form-group">
<label>Дата прохождения</label>
<input type="date" id="recordCompletedDate">
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal('recordModal')">Отмена</button>
<button type="submit" class="btn btn-primary">Сохранить</button>
</div>
</form>
</div>
</div>
<!-- Модальное окно: Курс -->
<div id="courseModal" class="modal-overlay hidden">
<div class="modal">
<h3 id="courseModalTitle">Новый курс</h3>
<form id="courseForm" onsubmit="saveCourse(event)">
<input type="hidden" id="courseId">
<div class="form-group">
<label>Название курса *</label>
<input type="text" id="courseName" required placeholder="Например: Кибербезопасность">
</div>
<div class="form-group">
<label>Категория</label>
<select id="courseCategory">
<option value="Обязательный">Обязательный</option>
<option value="Профессиональный">Профессиональный</option>
<option value="Развивающий">Развивающий</option>
<option value="По желанию">По желанию</option>
</select>
</div>
<div class="form-group">
<label>Описание</label>
<textarea id="courseDesc" rows="3" placeholder="Краткое описание курса"></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal('courseModal')">Отмена</button>
<button type="submit" class="btn btn-primary">Сохранить</button>
</div>
</form>
</div>
</div>
<!-- Модальное окно: Импорт -->
<div id="importModal" class="modal-overlay hidden">
<div class="modal">
<h3>Импорт данных</h3>
<div class="alert alert-info">
Загрузите CSV файл с полями: сотрудник, отдел, курс, дедлайн (YYYY-MM-DD), статус (pending/progress/completed), датаПрохождения (YYYY-MM-DD)
</div>
<div class="form-group">
<input type="file" id="importFile" accept=".csv,.xlsx,.xls">
</div>
<div id="importPreview" class="table-container hidden" style="max-height: 200px; overflow-y: auto; margin-bottom: 16px;">
<table>
<thead><tr><th>Сотрудник</th><th>Отдел</th><th>Курс</th><th>Статус</th></tr></thead>
<tbody id="importPreviewBody"></tbody>
</table>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal('importModal')">Отмена</button>
<button type="button" class="btn btn-primary" onclick="processImport()">Импортировать</button>
</div>
</div>
</div>
<script>
// === ДЕМО-ДАННЫЕ ===
const DEMO_COURSES = [
{ id: 1, name: "Кибербезопасность", category: "Обязательный", desc: "Основы информационной безопасности" },
{ id: 2, name: "Эффективные коммуникации", category: "Развивающий", desc: "Навыки делового общения" },
{ id: 3, name: "Excel для продвинутых", category: "Профессиональный", desc: "Сводные таблицы, макросы, формулы" },
{ id: 4, name: "Тайм-менеджмент", category: "Развивающий", desc: "Управление временем и приоритетами" },
{ id: 5, name: "Противодействие коррупции", category: "Обязательный", desc: "Комплаенс и этика бизнеса" },
{ id: 6, name: "Управление проектами", category: "Профессиональный", desc: "Методологии Agile и Waterfall" },
{ id: 7, name: "Охрана труда", category: "Обязательный", desc: "Техника безопасности на рабочем месте" }
];
const DEMO_EMPLOYEES = [
{ name: "Иванов Алексей", dept: "IT" }, { name: "Петрова Мария", dept: "IT" },
{ name: "Сидоров Дмитрий", dept: "IT" }, { name: "Кузнецова Ольга", dept: "HR" },
{ name: "Смирнов Андрей", dept: "HR" }, { name: "Васильев Игорь", dept: "Продажи" },
{ name: "Морозова Елена", dept: "Продажи" }, { name: "Новиков Сергей", dept: "Продажи" },
{ name: "Федорова Анна", dept: "Маркетинг" }, { name: "Козлов Максим", dept: "Маркетинг" },
{ name: "Павлова Наталья", dept: "Финансы" }, { name: "Соколов Артем", dept: "Финансы" },
{ name: "Михайлов Константин", dept: "Юридический" }, { name: "Ержанов Азамат", dept: "Операционный" },
{ name: "Ким Юлия", dept: "Безопасность" }, { name: "Трембаев Нурлан", dept: "IT" }
];
const DEMO_RECORDS = [
{ id: 1, employee: "Иванов Алексей", dept: "IT", courseId: 1, deadline: "2025-09-15", status: "completed", completedDate: "2025-09-10" },
{ id: 2, employee: "Иванов Алексей", dept: "IT", courseId: 3, deadline: "2025-10-01", status: "progress", completedDate: "" },
{ id: 3, employee: "Петрова Мария", dept: "IT", courseId: 1, deadline: "2025-09-15", status: "completed", completedDate: "2025-09-12" },
{ id: 4, employee: "Петрова Мария", dept: "IT", courseId: 6, deadline: "2025-11-20", status: "pending", completedDate: "" },
{ id: 5, employee: "Сидоров Дмитрий", dept: "IT", courseId: 1, deadline: "2025-09-15", status: "overdue", completedDate: "" },
{ id: 6, employee: "Кузнецова Ольга", dept: "HR", courseId: 2, deadline: "2025-10-10", status: "completed", completedDate: "2025-10-05" },
{ id: 7, employee: "Кузнецова Ольга", dept: "HR", courseId: 5, deadline: "2025-09-01", status: "overdue", completedDate: "" },
{ id: 8, employee: "Смирнов Андрей", dept: "HR", courseId: 2, deadline: "2025-10-10", status: "progress", completedDate: "" },
{ id: 9, employee: "Васильев Игорь", dept: "Продажи", courseId: 4, deadline: "2025-09-20", status: "completed", completedDate: "2025-09-18" },
{ id: 10, employee: "Васильев Игорь", dept: "Продажи", courseId: 3, deadline: "2025-10-15", status: "progress", completedDate: "" },
{ id: 11, employee: "Морозова Елена", dept: "Продажи", courseId: 4, deadline: "2025-09-20", status: "pending", completedDate: "" },
{ id: 12, employee: "Новиков Сергей", dept: "Продажи", courseId: 5, deadline: "2025-08-31", status: "overdue", completedDate: "" },
{ id: 13, employee: "Федорова Анна", dept: "Маркетинг", courseId: 6, deadline: "2025-11-01", status: "progress", completedDate: "" },
{ id: 14, employee: "Козлов Максим", dept: "Маркетинг", courseId: 2, deadline: "2025-10-10", status: "completed", completedDate: "2025-10-08" },
{ id: 15, employee: "Павлова Наталья", dept: "Финансы", courseId: 3, deadline: "2025-10-01", status: "completed", completedDate: "2025-09-28" },
{ id: 16, employee: "Соколов Артем", dept: "Финансы", courseId: 5, deadline: "2025-09-01", status: "overdue", completedDate: "" },
{ id: 17, employee: "Михайлов Константин", dept: "Юридический", courseId: 5, deadline: "2025-09-01", status: "completed", completedDate: "2025-08-25" },
{ id: 18, employee: "Михайлов Константин", dept: "Юридический", courseId: 7, deadline: "2025-10-20", status: "pending", completedDate: "" },
{ id: 19, employee: "Ержанов Азамат", dept: "Операционный", courseId: 7, deadline: "2025-09-30", status: "progress", completedDate: "" },
{ id: 20, employee: "Ким Юлия", dept: "Безопасность", courseId: 1, deadline: "2025-09-15", status: "completed", completedDate: "2025-09-14" },
{ id: 21, employee: "Трембаев Нурлан", dept: "IT", courseId: 7, deadline: "2025-10-05", status: "pending", completedDate: "" },
{ id: 22, employee: "Трембаев Нурлан", dept: "IT", courseId: 6, deadline: "2025-11-15", status: "progress", completedDate: "" }
];
// === СОСТОЯНИЕ ===
let courses = [];
let records = [];
let importedData = [];
// === ИНИЦИАЛИЗАЦИЯ ===
function init() {
const storedCourses = localStorage.getItem('tracker_courses');
const storedRecords = localStorage.getItem('tracker_records');
if (storedCourses) {
courses = JSON.parse(storedCourses);
} else {
courses = [...DEMO_COURSES];
saveCourses();
}
if (storedRecords) {
records = JSON.parse(storedRecords);
} else {
records = [...DEMO_RECORDS];
saveRecords();
}
populateFilters();
renderAll();
}
function saveCourses() {
localStorage.setItem('tracker_courses', JSON.stringify(courses));
}
function saveRecords() {
localStorage.setItem('tracker_records', JSON.stringify(records));
}
// === РЕНДЕРИНГ ===
function renderAll() {
renderStats();
renderRecords();
renderCourses();
renderDeptStats();
}
function renderStats() {
const today = new Date().toISOString().split('T')[0];
const totalRecords = records.length;
const completed = records.filter(r => r.status === 'completed').length;
const overdue = records.filter(r => r.status === 'overdue').length;
const progress = (totalRecords > 0) ? Math.round((completed / totalRecords) * 100) : 0;
document.getElementById('statCourses').textContent = courses.length;
document.getElementById('statRecords').textContent = totalRecords;
document.getElementById('statCompleted').textContent = completed;
document.getElementById('statOverdue').textContent = overdue;
const completedDelta = records.filter(r => r.status === 'completed' && r.completedDate && r.completedDate >= today).length;
document.getElementById('statCompletedDelta').textContent = completedDelta > 0 ? `+${completedDelta} за последнюю неделю` : '';
document.getElementById('statOverdueDelta').textContent = overdue > 0 ? `Требует внимания` : 'Все в срок';
document.getElementById('progressCompleted').style.width = progress + '%';
}
function renderRecords() {
const tbody = document.getElementById('recordsTable');
const empty = document.getElementById('recordsEmpty');
const filtered = getFilteredRecords();
if (filtered.length === 0) {
tbody.innerHTML = '';
empty.classList.remove('hidden');
return;
}
empty.classList.add('hidden');
tbody.innerHTML = filtered.map(r => {
const course = courses.find(c => c.id == r.courseId);
const statusClass = `status-${r.status}`;
const statusText = {
'pending': 'Запланировано',
'progress': 'В процессе',
'completed': 'Пройдено',
'overdue': 'Просрочено'
}[r.status];
return `
<tr>
<td><strong>${r.employee}</strong></td>
<td>${r.dept}</td>
<td>${course ? course.name : 'Удалён'}</td>
<td>${formatDate(r.deadline)}</td>
<td><span class="status ${statusClass}">${statusText}</span></td>
<td>${r.completedDate ? formatDate(r.completedDate) : '—'}</td>
<td>
<button class="btn btn-sm ${r.status !== 'completed' ? 'btn-success' : 'btn-secondary'}"
onclick="markCompleted(${r.id})"
${r.status === 'completed' ? 'disabled' : ''}>
${r.status === 'completed' ? '✓' : 'Отметить'}
</button>
<button class="btn btn-sm btn-secondary" onclick="editRecord(${r.id})">✏️</button>
<button class="btn btn-sm btn-danger" onclick="deleteRecord(${r.id})">🗑️</button>
</td>
</tr>
`;
}).join('');
}
function renderCourses() {
const grid = document.getElementById('coursesGrid');
const empty = document.getElementById('coursesEmpty');
if (courses.length === 0) {
grid.innerHTML = '';
empty.classList.remove('hidden');
return;
}
empty.classList.add('hidden');
grid.innerHTML = courses.map(c => {
const enrolled = records.filter(r => r.courseId == c.id).length;
const completed = records.filter(r => r.courseId == c.id && r.status === 'completed').length;
const overdue = records.filter(r => r.courseId == c.id && r.status === 'overdue').length;
const progress = enrolled > 0 ? Math.round((completed / enrolled) * 100) : 0;
return `
<div class="course-card" onclick="showCourseDetails(${c.id})">
<h4>${c.name}</h4>
<div class="meta">${c.category} • ${c.desc || ''}</div>
<div class="course-stats">
<div class="course-stat">
<span class="num">${enrolled}</span>
<span>Записано</span>
</div>
<div class="course-stat">
<span class="num" style="color: var(--success)">${completed}</span>
<span>Пройдено</span>
</div>
<div class="course-stat">
<span class="num" style="color: var(--danger)">${overdue}</span>
<span>Просрочено</span>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
</div>
</div>
`;
}).join('');
}
function renderDeptStats() {
const container = document.getElementById('deptStats');
const depts = {};
records.forEach(r => {
if (!depts[r.dept]) {
depts[r.dept] = { total: 0, completed: 0, progress: 0, overdue: 0 };
}
depts[r.dept].total++;
if (r.status === 'completed') depts[r.dept].completed++;
else if (r.status === 'progress') depts[r.dept].progress++;
else if (r.status === 'overdue') depts[r.dept].overdue++;
else if (r.status === 'pending') depts[r.dept].progress++;
});
container.innerHTML = Object.entries(depts).map(([dept, stats]) => {
const progress = stats.total > 0 ? Math.round((stats.completed / stats.total) * 100) : 0;
return `
<div class="dept-card">
<h4>🏢 ${dept}</h4>
<div class="dept-row">
<span>Всего записей</span>
<strong>${stats.total}</strong>
</div>
<div class="dept-row">
<span>Пройдено</span>
<span style="color: var(--success)">${stats.completed}</span>
</div>
<div class="dept-row">
<span>В процессе</span>
<span style="color: var(--primary)">${stats.progress}</span>
</div>
<div class="dept-row">
<span>Просрочено</span>
<span style="color: var(--danger)">${stats.overdue}</span>
</div>
<div class="progress-bar" style="margin-top: 12px;">
<div class="progress-fill" style="width: ${progress}%; background: ${progress >= 80 ? 'var(--success)' : progress >= 50 ? 'var(--warning)' : 'var(--danger)'}"></div>
</div>
</div>
`;
}).join('');
}
// === ФИЛЬТРЫ ===
function populateFilters() {
const deptSelect = document.getElementById('filterDept');
const courseSelect = document.getElementById('filterCourse');
const recordCourse = document.getElementById('recordCourse');
const depts = [...new Set(records.map(r => r.dept))].sort();
deptSelect.innerHTML = '<option value="">Все отделы</option>' +
depts.map(d => `<option value="${d}">${d}</option>`).join('');
courseSelect.innerHTML = '<option value="">Все курсы</option>' +
courses.map(c => `<option value="${c.id}">${c.name}</option>`).join('');
recordCourse.innerHTML = '<option value="">Выберите курс</option>' +
courses.map(c => `<option value="${c.id}">${c.name}</option>`).join('');
}
function getFilteredRecords() {
const search = document.getElementById('filterSearch').value.toLowerCase();
const dept = document.getElementById('filterDept').value;
const course = document.getElementById('filterCourse').value;
const status = document.getElementById('filterStatus').value;
return records.filter(r => {
if (search && !r.employee.toLowerCase().includes(search)) return false;
if (dept && r.dept !== dept) return false;
if (course && r.courseId != course) return false;
if (status && r.status !== status) return false;
return true;
});
}
function applyFilters() {
renderRecords();
}
// === МОДАЛЬНЫЕ ОКНА ===
function openRecordModal(id = null) {
const modal = document.getElementById('recordModal');
const form = document.getElementById('recordForm');
form.reset();
document.getElementById('recordId').value = '';
document.getElementById('recordModalTitle').textContent = 'Новая запись на обучение';
document.getElementById('recordCompletedDate').parentElement.classList.add('hidden');
if (id) {
const r = records.find(x => x.id === id);
if (r) {
document.getElementById('recordId').value = r.id;
document.getElementById('recordEmployee').value = r.employee;
document.getElementById('recordDept').value = r.dept;
document.getElementById('recordCourse').value = r.courseId;
document.getElementById('recordStatus').value = r.status;
document.getElementById('recordDeadline').value = r.deadline;
document.getElementById('recordCompletedDate').value = r.completedDate || '';
document.getElementById('recordModalTitle').textContent = 'Редактирование записи';
toggleCompletionDate();
}
}
modal.classList.remove('hidden');
}
function openCourseModal(id = null) {
const modal = document.getElementById('courseModal');
const form = document.getElementById('courseForm');
form.reset();
document.getElementById('courseId').value = '';
document.getElementById('courseModalTitle').textContent = 'Новый курс';
if (id) {
const c = courses.find(x => x.id === id);
if (c) {
document.getElementById('courseId').value = c.id;
document.getElementById('courseName').value = c.name;
document.getElementById('courseCategory').value = c.category;
document.getElementById('courseDesc').value = c.desc || '';
document.getElementById('courseModalTitle').textContent = 'Редактирование курса';
}
}
modal.classList.remove('hidden');
}
function openImportModal() {
document.getElementById('importFile').value = '';
document.getElementById('importPreview').classList.add('hidden');
document.getElementById('importModal').classList.remove('hidden');
importedData = [];
}
function closeModal(modalId) {
document.getElementById(modalId).classList.add('hidden');
}
function toggleCompletionDate() {
const status = document.getElementById('recordStatus').value;
const field = document.getElementById('recordCompletedDate').parentElement;
if (status === 'completed') {
field.classList.remove('hidden');
if (!document.getElementById('recordCompletedDate').value) {
document.getElementById('recordCompletedDate').value = new Date().toISOString().split('T')[0];
}
} else {
field.classList.add('hidden');
document.getElementById('recordCompletedDate').value = '';
}
}
// === СОХРАНЕНИЕ ===
function saveRecord(e) {
e.preventDefault();
const id = document.getElementById('recordId').value;
const status = document.getElementById('recordStatus').value;
const record = {
employee: document.getElementById('recordEmployee').value,
dept: document.getElementById('recordDept').value,
courseId: parseInt(document.getElementById('recordCourse').value),
deadline: document.getElementById('recordDeadline').value,
status: status,
completedDate: status === 'completed' ? document.getElementById('recordCompletedDate').value : ''
};
if (id) {
const idx = records.findIndex(r => r.id == id);
if (idx !== -1) {
record.id = parseInt(id);
records[idx] = record;
}
} else {
record.id = Date.now();
records.push(record);
}
saveRecords();
populateFilters();
renderAll();
closeModal('recordModal');
}
function saveCourse(e) {
e.preventDefault();
const id = document.getElementById('courseId').value;
const course = {
name: document.getElementById('courseName').value,
category: document.getElementById('courseCategory').value,
desc: document.getElementById('courseDesc').value
};
if (id) {
const idx = courses.findIndex(c => c.id == id);
if (idx !== -1) {
course.id = parseInt(id);
courses[idx] = course;
}
} else {
course.id = Date.now();
courses.push(course);
}
saveCourses();
populateFilters();
renderAll();
closeModal('courseModal');
}
// === ДЕЙСТВИЯ ===
function markCompleted(id) {
const r = records.find(x => x.id === id);
if (r) {
r.status = 'completed';
r.completedDate = new Date().toISOString().split('T')[0];
saveRecords();
renderAll();
}
}
function editRecord(id) {
openRecordModal(id);
}
function deleteRecord(id) {
if (confirm('Удалить эту запись?')) {
records = records.filter(r => r.id !== id);
saveRecords();
renderAll();
}
}
function showCourseDetails(id) {
const course = courses.find(c => c.id === id);
if (!course) return;
document.getElementById('filterCourse').value = id;
switchTab('records');
applyFilters();
}
// === ИМПОРТ/ЭКСПОРТ ===
document.getElementById('importFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const wb = XLSX.read(e.target.result, { type: 'array' });
const ws = wb.Sheets[wb.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(ws);
importedData = data.map((row, i) => ({
...row,
_row: i + 2
}));
const preview = document.getElementById('importPreviewBody');
preview.innerHTML = importedData.slice(0, 5).map(row => `
<tr>
<td>${row.сотрудник || row.employee || '—'}</td>
<td>${row.отдел || row.dept || '—'}</td>
<td>${row.курс || row.course || '—'}</td>
<td>${row.статус || row.status || '—'}</td>
</tr>
`).join('');
if (importedData.length > 5) {
preview.innerHTML += `<tr><td colspan="4">... ещё ${importedData.length - 5} строк</td></tr>`;
}
document.getElementById('importPreview').classList.remove('hidden');
} catch (err) {
alert('Ошибка чтения файла: ' + err.message);
}
};
reader.readAsArrayBuffer(file);
});
function processImport() {
if (importedData.length === 0) {
alert('Выберите файл для импорта');
return;
}
importedData.forEach(row => {
const courseName = row.курс || row.course;
let courseId = courses.find(c => c.name === courseName)?.id;
if (!courseId && courseName) {
courseId = Date.now() + Math.random();
courses.push({
id: courseId,
name: courseName,
category: 'Импорт',
desc: 'Добавлен через импорт'
});
}
records.push({
id: Date.now() + Math.random(),
employee: row.сотрудник || row.employee || 'Неизвестно',
dept: row.отдел || row.dept || 'Без отдела',
courseId: courseId || 0,
deadline: row.дедлайн || row.deadline || new Date().toISOString().split('T')[0],
status: row.статус || row.status || 'pending',
completedDate: row.датаПрохождения || row.completedDate || ''
});
});
saveCourses();
saveRecords();
populateFilters();
renderAll();
closeModal('importModal');
alert(`Импортировано ${importedData.length} записей`);
}
function exportToExcel() {
const today = new Date().toISOString().split('T')[0];
const wsData = records.map(r => {
const course = courses.find(c => c.id == r.courseId);
return {
'Сотрудник': r.employee,
'Отдел': r.dept,
'Курс': course ? course.name : 'Удалён',
'Дедлайн': r.deadline,
'Статус': {
'pending': 'Запланировано',
'progress': 'В процессе',
'completed': 'Пройдено',
'overdue': 'Просрочено'
}[r.status],
'Дата прохождения': r.completedDate || '',
'Просрочено': r.status === 'overdue' || (r.status !== 'completed' && r.deadline < today) ? 'Да' : 'Нет'
};
});
const ws = XLSX.utils.json_to_sheet(wsData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Записи');
const deptData = [];
const depts = {};
records.forEach(r => {
if (!depts[r.dept]) depts[r.dept] = { total: 0, completed: 0, overdue: 0 };
depts[r.dept].total++;
if (r.status === 'completed') depts[r.dept].completed++;
if (r.status === 'overdue') depts[r.dept].overdue++;
});
Object.entries(depts).forEach(([dept, stats]) => {
deptData.push({
'Отдел': dept,
'Всего': stats.total,
'Пройдено': stats.completed,
'Просрочено': stats.overdue,
'% завершения': stats.total > 0 ? Math.round((stats.completed / stats.total) * 100) + '%' : '0%'
});
});
const wsDept = XLSX.utils.json_to_sheet(deptData);
XLSX.utils.book_append_sheet(wb, wsDept, 'По отделам');
XLSX.writeFile(wb, `Трекер_обучения_${today}.xlsx`);
}
// === УТИЛИТЫ ===
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('[id^="tab-"]').forEach(t => t.classList.add('hidden'));
event.target.classList.add('active');
document.getElementById('tab-' + tab).classList.remove('hidden');
if (tab === 'departments') renderDeptStats();
}
function formatDate(dateStr) {
if (!dateStr) return '—';
const d = new Date(dateStr);
return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: '2-digit' });
}
// Запуск
init();
</script>
</body>
</html>