604 lines
26 KiB
HTML
604 lines
26 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru" data-theme="light">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Трекер задач отдела</title>
|
||
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-components.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-page.css">
|
||
<style>
|
||
.app-container { min-height: 100vh; background: var(--kt-ai-bg); }
|
||
.app-header { position: sticky; top: 0; z-index: 100; background: var(--kt-ai-bg); border-bottom: 1px solid var(--kt-ai-divider); padding: var(--kt-ai-space-4) 0; }
|
||
.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-6); }
|
||
.stat-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5); text-align: center; }
|
||
.stat-value { font-size: var(--kt-ai-text-3xl); font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-fg); margin-bottom: var(--kt-ai-space-1); }
|
||
.stat-label { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); }
|
||
.filters-bar { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-3); margin-bottom: var(--kt-ai-space-5); align-items: center; }
|
||
.search-input { flex: 1; min-width: 200px; }
|
||
.chip-group { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-2); }
|
||
.tasks-table { width: 100%; border-collapse: collapse; background: var(--kt-ai-card-bg); border-radius: var(--kt-ai-radius-xl); overflow: hidden; }
|
||
.tasks-table th, .tasks-table td { padding: var(--kt-ai-space-4); text-align: left; border-bottom: 1px solid var(--kt-ai-divider); }
|
||
.tasks-table th { background: var(--kt-ai-bg-soft); font-weight: var(--kt-ai-weight-semibold); color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); }
|
||
.tasks-table tr:hover { background: var(--kt-ai-bg-soft); }
|
||
.tasks-table tr:last-child td { border-bottom: none; }
|
||
.status-select { padding: var(--kt-ai-space-2) var(--kt-ai-space-3); border-radius: var(--kt-ai-radius-md); border: 1px solid var(--kt-ai-border); background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-size: var(--kt-ai-text-sm); cursor: pointer; }
|
||
.action-btns { display: flex; gap: var(--kt-ai-space-2); }
|
||
.icon-btn { padding: var(--kt-ai-space-2); border: none; background: transparent; color: var(--kt-ai-fg-muted); cursor: pointer; border-radius: var(--kt-ai-radius-md); transition: all 0.2s; }
|
||
.icon-btn:hover { background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg); }
|
||
.icon-btn.danger:hover { color: var(--kt-ai-danger); background: rgba(239, 68, 68, 0.1); }
|
||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000; display: none; align-items: center; justify-content: center; }
|
||
.modal-overlay.active { display: flex; }
|
||
.modal { background: var(--kt-ai-card-bg); border-radius: var(--kt-ai-radius-2xl); padding: var(--kt-ai-space-6); width: 100%; max-width: 500px; max-height: 90vh; overflow-y: auto; }
|
||
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--kt-ai-space-5); }
|
||
.modal-title { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); }
|
||
.close-btn { background: transparent; border: none; font-size: var(--kt-ai-text-2xl); color: var(--kt-ai-fg-muted); cursor: pointer; padding: 0; line-height: 1; }
|
||
.close-btn:hover { color: var(--kt-ai-fg); }
|
||
.form-group { margin-bottom: var(--kt-ai-space-4); }
|
||
.form-label { display: block; margin-bottom: var(--kt-ai-space-2); font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg); }
|
||
.form-input, .form-select, .form-textarea { width: 100%; padding: var(--kt-ai-space-3); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-md); background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-size: var(--kt-ai-text-base); }
|
||
.form-input:focus, .form-select:focus, .form-textarea:focus { outline: 2px solid var(--kt-ai-primary); border-color: transparent; }
|
||
.form-textarea { min-height: 100px; resize: vertical; }
|
||
.tabs { display: flex; gap: var(--kt-ai-space-1); margin-bottom: var(--kt-ai-space-5); border-bottom: 1px solid var(--kt-ai-divider); }
|
||
.tab { padding: var(--kt-ai-space-3) var(--kt-ai-space-4); background: transparent; border: none; color: var(--kt-ai-fg-muted); cursor: pointer; font-weight: var(--kt-ai-weight-medium); border-bottom: 2px solid transparent; margin-bottom: -1px; }
|
||
.tab:hover { color: var(--kt-ai-fg); }
|
||
.tab.active { color: var(--kt-ai-primary); border-bottom-color: var(--kt-ai-primary); }
|
||
.log-entry { padding: var(--kt-ai-space-3); border-left: 3px solid var(--kt-ai-primary); background: var(--kt-ai-bg-soft); margin-bottom: var(--kt-ai-space-3); border-radius: 0 var(--kt-ai-radius-md) var(--kt-ai-radius-md) 0; }
|
||
.log-time { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-1); }
|
||
.log-action { font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg); }
|
||
.log-details { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); }
|
||
.empty-state { text-align: center; padding: var(--kt-ai-space-10); color: var(--kt-ai-fg-muted); }
|
||
.priority-high { color: var(--kt-ai-danger); }
|
||
.priority-medium { color: #f59e0b; }
|
||
.priority-low { color: #10b981; }
|
||
.header-actions { display: flex; gap: var(--kt-ai-space-3); align-items: center; }
|
||
.theme-toggle { background: transparent; border: 1px solid var(--kt-ai-border); padding: var(--kt-ai-space-2); border-radius: var(--kt-ai-radius-md); cursor: pointer; color: var(--kt-ai-fg); }
|
||
@media (max-width: 768px) {
|
||
.tasks-table { font-size: var(--kt-ai-text-sm); }
|
||
.tasks-table th, .tasks-table td { padding: var(--kt-ai-space-2); }
|
||
.dashboard-grid { grid-template-columns: repeat(2, 1fr); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-container">
|
||
<header class="app-header">
|
||
<div class="kt-ai-wrap" style="display:flex;align-items:center;justify-content:space-between">
|
||
<h1 style="font-size:var(--kt-ai-text-xl);font-weight:var(--kt-ai-weight-semibold);margin:0">Трекер задач отдела</h1>
|
||
<div class="header-actions">
|
||
<button class="kt-ai-btn" data-variant="ghost" id="exportBtn">Выгрузить в Excel</button>
|
||
<button class="kt-ai-btn" data-variant="primary" id="addTaskBtn">Добавить задачу</button>
|
||
<button class="theme-toggle" id="themeToggle" title="Сменить тему">◐</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="kt-ai-wrap" style="padding:var(--kt-ai-space-6) 0">
|
||
<div class="dashboard-grid" id="dashboard">
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="statTotal">0</div>
|
||
<div class="stat-label">Всего задач</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="statNew">0</div>
|
||
<div class="stat-label">Новые</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="statInProgress">0</div>
|
||
<div class="stat-label">В работе</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="statReview">0</div>
|
||
<div class="stat-label">На проверке</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="statDone">0</div>
|
||
<div class="stat-label">Завершено</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tabs">
|
||
<button class="tab active" data-tab="tasks">Задачи</button>
|
||
<button class="tab" data-tab="log">Журнал изменений</button>
|
||
</div>
|
||
|
||
<div id="tasksTab">
|
||
<div class="filters-bar">
|
||
<input type="text" class="kt-ai-input search-input" id="searchInput" placeholder="Поиск по названию задачи...">
|
||
<select class="kt-ai-select" id="employeeFilter" style="min-width:180px">
|
||
<option value="">Все сотрудники</option>
|
||
</select>
|
||
<div class="chip-group" id="statusChips"></div>
|
||
</div>
|
||
|
||
<table class="tasks-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Название</th>
|
||
<th>Ответственный</th>
|
||
<th>Приоритет</th>
|
||
<th>Срок</th>
|
||
<th>Статус</th>
|
||
<th style="width:100px">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tasksBody"></tbody>
|
||
</table>
|
||
|
||
<div class="empty-state" id="emptyState" style="display:none">
|
||
<p style="font-size:var(--kt-ai-text-lg)">Нет задач</p>
|
||
<p>Нажмите «Добавить задачу», чтобы создать первую</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="logTab" style="display:none">
|
||
<div id="logContainer"></div>
|
||
<div class="empty-state" id="emptyLog" style="display:none">
|
||
<p>Журнал пуст</p>
|
||
<p>Здесь появятся все изменения задач</p>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Modal: Add/Edit Task -->
|
||
<div class="modal-overlay" id="taskModal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h2 class="modal-title" id="modalTitle">Новая задача</h2>
|
||
<button class="close-btn" id="closeModal">×</button>
|
||
</div>
|
||
<form id="taskForm">
|
||
<input type="hidden" id="taskId">
|
||
<div class="form-group">
|
||
<label class="form-label" for="taskTitle">Название задачи *</label>
|
||
<input type="text" class="form-input" id="taskTitle" required placeholder="Что нужно сделать?">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label" for="taskEmployee">Ответственный *</label>
|
||
<input type="text" class="form-input" id="taskEmployee" required placeholder="Фамилия И.">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label" for="taskPriority">Приоритет</label>
|
||
<select class="form-select" id="taskPriority">
|
||
<option value="Низкий">Низкий</option>
|
||
<option value="Средний" selected>Средний</option>
|
||
<option value="Высокий">Высокий</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label" for="taskDeadline">Срок выполнения</label>
|
||
<input type="date" class="form-input" id="taskDeadline">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label" for="taskDescription">Описание</label>
|
||
<textarea class="form-textarea" id="taskDescription" placeholder="Детали задачи..."></textarea>
|
||
</div>
|
||
<div style="display:flex;gap:var(--kt-ai-space-3);justify-content:flex-end;margin-top:var(--kt-ai-space-6)">
|
||
<button type="button" class="kt-ai-btn" data-variant="ghost" id="cancelBtn">Отмена</button>
|
||
<button type="submit" class="kt-ai-btn" data-variant="primary">Сохранить</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: Confirm Delete -->
|
||
<div class="modal-overlay" id="deleteModal">
|
||
<div class="modal" style="max-width:400px">
|
||
<div class="modal-header">
|
||
<h2 class="modal-title">Подтверждение</h2>
|
||
<button class="close-btn" id="closeDelete">×</button>
|
||
</div>
|
||
<p style="margin-bottom:var(--kt-ai-space-5)">Вы уверены, что хотите удалить эту задачу?</p>
|
||
<div style="display:flex;gap:var(--kt-ai-space-3);justify-content:flex-end">
|
||
<button type="button" class="kt-ai-btn" data-variant="ghost" id="cancelDelete">Отмена</button>
|
||
<button type="button" class="kt-ai-btn" data-variant="danger" id="confirmDelete">Удалить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Демо-данные
|
||
const DEMO_TASKS = [
|
||
{ id: 1, title: 'Подготовить отчёт за квартал', employee: 'Алиев А.', priority: 'Высокий', deadline: '2026-09-10', status: 'В работе', description: 'Сводный отчёт по отделу' },
|
||
{ id: 2, title: 'Обновить документацию', employee: 'Смирнова Е.', priority: 'Средний', deadline: '2026-09-15', status: 'Новая', description: 'Актуализировать инструкции' },
|
||
{ id: 3, title: 'Провести обучение новичков', employee: 'Иванов П.', priority: 'Высокий', deadline: '2026-09-08', status: 'На проверке', description: 'Вводный курс для новых сотрудников' },
|
||
{ id: 4, title: 'Настроить резервное копирование', employee: 'Алиев А.', priority: 'Высокий', deadline: '2026-09-05', status: 'Завершено', description: 'Ежедневный бэкап данных' },
|
||
{ id: 5, title: 'Оптимизировать базу данных', employee: 'Ким Д.', priority: 'Средний', deadline: '2026-09-20', status: 'В работе', description: 'Ускорить выборку данных' },
|
||
{ id: 6, title: 'Подготовить презентацию', employee: 'Смирнова Е.', priority: 'Низкий', deadline: '2026-09-25', status: 'Новая', description: 'Для руководства компании' }
|
||
];
|
||
|
||
const STATUSES = ['Новая', 'В работе', 'На проверке', 'Завершено'];
|
||
const EMPLOYEES = ['Алиев А.', 'Смирнова Е.', 'Иванов П.', 'Ким Д.'];
|
||
|
||
// Состояние приложения
|
||
let tasks = [];
|
||
let log = [];
|
||
let currentFilter = '';
|
||
let employeeFilter = '';
|
||
let taskToDelete = null;
|
||
|
||
// Инициализация
|
||
function init() {
|
||
const stored = localStorage.getItem('trackerTasks');
|
||
const storedLog = localStorage.getItem('trackerLog');
|
||
|
||
if (stored) {
|
||
tasks = JSON.parse(stored);
|
||
} else {
|
||
tasks = [...DEMO_TASKS];
|
||
saveTasks();
|
||
}
|
||
|
||
if (storedLog) {
|
||
log = JSON.parse(storedLog);
|
||
}
|
||
|
||
renderDashboard();
|
||
renderTasks();
|
||
renderEmployeeFilter();
|
||
renderStatusChips();
|
||
renderLog();
|
||
setupEventListeners();
|
||
updateTheme();
|
||
}
|
||
|
||
// Сохранение
|
||
function saveTasks() {
|
||
localStorage.setItem('trackerTasks', JSON.stringify(tasks));
|
||
renderDashboard();
|
||
}
|
||
|
||
function saveLog() {
|
||
localStorage.setItem('trackerLog', JSON.stringify(log));
|
||
renderLog();
|
||
}
|
||
|
||
function addToLog(action, details) {
|
||
log.unshift({
|
||
time: new Date().toISOString(),
|
||
action: action,
|
||
details: details
|
||
});
|
||
saveLog();
|
||
}
|
||
|
||
// Рендер дашборда
|
||
function renderDashboard() {
|
||
document.getElementById('statTotal').textContent = tasks.length;
|
||
document.getElementById('statNew').textContent = tasks.filter(t => t.status === 'Новая').length;
|
||
document.getElementById('statInProgress').textContent = tasks.filter(t => t.status === 'В работе').length;
|
||
document.getElementById('statReview').textContent = tasks.filter(t => t.status === 'На проверке').length;
|
||
document.getElementById('statDone').textContent = tasks.filter(t => t.status === 'Завершено').length;
|
||
}
|
||
|
||
// Рендер задач
|
||
function renderTasks() {
|
||
const tbody = document.getElementById('tasksBody');
|
||
const emptyState = document.getElementById('emptyState');
|
||
|
||
let filtered = tasks.filter(t => {
|
||
const matchSearch = !currentFilter || t.title.toLowerCase().includes(currentFilter.toLowerCase());
|
||
const matchEmployee = !employeeFilter || t.employee === employeeFilter;
|
||
return matchSearch && matchEmployee;
|
||
});
|
||
|
||
if (filtered.length === 0) {
|
||
tbody.innerHTML = '';
|
||
emptyState.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
emptyState.style.display = 'none';
|
||
tbody.innerHTML = filtered.map(task => {
|
||
const priorityClass = task.priority === 'Высокий' ? 'priority-high' : task.priority === 'Средний' ? 'priority-medium' : 'priority-low';
|
||
const deadline = task.deadline ? new Date(task.deadline).toLocaleDateString('ru-RU') : '—';
|
||
return `
|
||
<tr>
|
||
<td>
|
||
<div style="font-weight:var(--kt-ai-weight-medium)">${escapeHtml(task.title)}</div>
|
||
${task.description ? `<div style="font-size:var(--kt-ai-text-sm);color:var(--kt-ai-fg-muted);margin-top:4px">${escapeHtml(task.description)}</div>` : ''}
|
||
</td>
|
||
<td>${escapeHtml(task.employee)}</td>
|
||
<td><span class="kt-ai-chip ${priorityClass}" style="font-size:var(--kt-ai-text-xs)">${task.priority}</span></td>
|
||
<td>${deadline}</td>
|
||
<td>
|
||
<select class="status-select" data-id="${task.id}">
|
||
${STATUSES.map(s => `<option value="${s}" ${task.status === s ? 'selected' : ''}>${s}</option>`).join('')}
|
||
</select>
|
||
</td>
|
||
<td>
|
||
<div class="action-btns">
|
||
<button class="icon-btn" onclick="editTask(${task.id})" title="Редактировать">✏️</button>
|
||
<button class="icon-btn danger" onclick="confirmDelete(${task.id})" title="Удалить">🗑️</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
|
||
// Обработчики статусов
|
||
tbody.querySelectorAll('.status-select').forEach(select => {
|
||
select.addEventListener('change', (e) => {
|
||
const taskId = parseInt(e.target.dataset.id);
|
||
const newStatus = e.target.value;
|
||
const task = tasks.find(t => t.id === taskId);
|
||
if (task) {
|
||
const oldStatus = task.status;
|
||
task.status = newStatus;
|
||
saveTasks();
|
||
addToLog('Изменение статуса', `Задача "${task.title}": ${oldStatus} → ${newStatus}`);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Рендер фильтра сотрудников
|
||
function renderEmployeeFilter() {
|
||
const select = document.getElementById('employeeFilter');
|
||
const employees = [...new Set(tasks.map(t => t.employee))].sort();
|
||
select.innerHTML = '<option value="">Все сотрудники</option>' +
|
||
employees.map(e => `<option value="${escapeHtml(e)}">${escapeHtml(e)}</option>`).join('');
|
||
}
|
||
|
||
// Рендер чипсов статусов
|
||
function renderStatusChips() {
|
||
const container = document.getElementById('statusChips');
|
||
container.innerHTML = STATUSES.map(status =>
|
||
`<button class="kt-ai-chip" data-status="${status}" style="cursor:pointer;border:none">${status}</button>`
|
||
).join('');
|
||
|
||
container.querySelectorAll('.kt-ai-chip').forEach(chip => {
|
||
chip.addEventListener('click', () => {
|
||
const status = chip.dataset.status;
|
||
if (currentFilter === status) {
|
||
currentFilter = '';
|
||
chip.classList.remove('active');
|
||
chip.style.background = '';
|
||
} else {
|
||
document.querySelectorAll('#statusChips .kt-ai-chip').forEach(c => {
|
||
c.classList.remove('active');
|
||
c.style.background = '';
|
||
});
|
||
currentFilter = status;
|
||
chip.classList.add('active');
|
||
chip.style.background = 'var(--kt-ai-primary)';
|
||
chip.style.color = '#fff';
|
||
}
|
||
renderTasks();
|
||
});
|
||
});
|
||
}
|
||
|
||
// Рендер журнала
|
||
function renderLog() {
|
||
const container = document.getElementById('logContainer');
|
||
const emptyLog = document.getElementById('emptyLog');
|
||
|
||
if (log.length === 0) {
|
||
container.innerHTML = '';
|
||
emptyLog.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
emptyLog.style.display = 'none';
|
||
container.innerHTML = log.map(entry => {
|
||
const time = new Date(entry.time).toLocaleString('ru-RU');
|
||
return `
|
||
<div class="log-entry">
|
||
<div class="log-time">${time}</div>
|
||
<div class="log-action">${escapeHtml(entry.action)}</div>
|
||
<div class="log-details">${escapeHtml(entry.details)}</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Экспорт в CSV
|
||
function exportToCSV() {
|
||
const BOM = '\uFEFF';
|
||
const headers = ['Название', 'Ответственный', 'Приоритет', 'Срок', 'Статус', 'Описание'];
|
||
const rows = tasks.map(t => [
|
||
t.title,
|
||
t.employee,
|
||
t.priority,
|
||
t.deadline || '',
|
||
t.status,
|
||
t.description || ''
|
||
].map(v => '"' + String(v || '').replace(/"/g, '""') + '"').join(';'));
|
||
|
||
const csv = BOM + [headers.join(';'), ...rows].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 = 'zadachi_otdela.csv';
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
// Модальное окно
|
||
function openModal(taskId = null) {
|
||
const modal = document.getElementById('taskModal');
|
||
const title = document.getElementById('modalTitle');
|
||
const form = document.getElementById('taskForm');
|
||
|
||
form.reset();
|
||
|
||
if (taskId) {
|
||
const task = tasks.find(t => t.id === taskId);
|
||
if (task) {
|
||
title.textContent = 'Редактирование задачи';
|
||
document.getElementById('taskId').value = task.id;
|
||
document.getElementById('taskTitle').value = task.title;
|
||
document.getElementById('taskEmployee').value = task.employee;
|
||
document.getElementById('taskPriority').value = task.priority;
|
||
document.getElementById('taskDeadline').value = task.deadline;
|
||
document.getElementById('taskDescription').value = task.description || '';
|
||
}
|
||
} else {
|
||
title.textContent = 'Новая задача';
|
||
document.getElementById('taskId').value = '';
|
||
}
|
||
|
||
modal.classList.add('active');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('taskModal').classList.remove('active');
|
||
}
|
||
|
||
// Редактирование
|
||
window.editTask = function(id) {
|
||
openModal(id);
|
||
};
|
||
|
||
// Подтверждение удаления
|
||
window.confirmDelete = function(id) {
|
||
taskToDelete = id;
|
||
document.getElementById('deleteModal').classList.add('active');
|
||
};
|
||
|
||
function closeDeleteModal() {
|
||
document.getElementById('deleteModal').classList.remove('active');
|
||
taskToDelete = null;
|
||
}
|
||
|
||
function deleteTask() {
|
||
if (taskToDelete) {
|
||
const task = tasks.find(t => t.id === taskToDelete);
|
||
if (task) {
|
||
addToLog('Удаление задачи', `"${task.title}" (ответственный: ${task.employee})`);
|
||
tasks = tasks.filter(t => t.id !== taskToDelete);
|
||
saveTasks();
|
||
renderTasks();
|
||
renderEmployeeFilter();
|
||
}
|
||
closeDeleteModal();
|
||
}
|
||
}
|
||
|
||
// Обработчики событий
|
||
function setupEventListeners() {
|
||
// Добавление задачи
|
||
document.getElementById('addTaskBtn').addEventListener('click', () => openModal());
|
||
document.getElementById('closeModal').addEventListener('click', closeModal);
|
||
document.getElementById('cancelBtn').addEventListener('click', closeModal);
|
||
|
||
// Форма
|
||
document.getElementById('taskForm').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
const id = document.getElementById('taskId').value;
|
||
const taskData = {
|
||
title: document.getElementById('taskTitle').value.trim(),
|
||
employee: document.getElementById('taskEmployee').value.trim(),
|
||
priority: document.getElementById('taskPriority').value,
|
||
deadline: document.getElementById('taskDeadline').value,
|
||
description: document.getElementById('taskDescription').value.trim(),
|
||
status: 'Новая'
|
||
};
|
||
|
||
if (id) {
|
||
// Редактирование
|
||
const index = tasks.findIndex(t => t.id === parseInt(id));
|
||
if (index !== -1) {
|
||
const oldTask = tasks[index];
|
||
taskData.id = parseInt(id);
|
||
taskData.status = oldTask.status;
|
||
tasks[index] = taskData;
|
||
addToLog('Редактирование задачи', `Изменены поля задачи "${taskData.title}"`);
|
||
}
|
||
} else {
|
||
// Создание
|
||
const newTask = {
|
||
...taskData,
|
||
id: Date.now()
|
||
};
|
||
tasks.unshift(newTask);
|
||
addToLog('Создание задачи', `"${newTask.title}" (ответственный: ${newTask.employee})`);
|
||
}
|
||
|
||
saveTasks();
|
||
renderTasks();
|
||
renderEmployeeFilter();
|
||
closeModal();
|
||
});
|
||
|
||
// Удаление
|
||
document.getElementById('closeDelete').addEventListener('click', closeDeleteModal);
|
||
document.getElementById('cancelDelete').addEventListener('click', closeDeleteModal);
|
||
document.getElementById('confirmDelete').addEventListener('click', deleteTask);
|
||
|
||
// Поиск
|
||
document.getElementById('searchInput').addEventListener('input', (e) => {
|
||
currentFilter = e.target.value;
|
||
renderTasks();
|
||
});
|
||
|
||
// Фильтр сотрудников
|
||
document.getElementById('employeeFilter').addEventListener('change', (e) => {
|
||
employeeFilter = e.target.value;
|
||
renderTasks();
|
||
});
|
||
|
||
// Табы
|
||
document.querySelectorAll('.tab').forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
|
||
if (tab.dataset.tab === 'tasks') {
|
||
document.getElementById('tasksTab').style.display = 'block';
|
||
document.getElementById('logTab').style.display = 'none';
|
||
} else {
|
||
document.getElementById('tasksTab').style.display = 'none';
|
||
document.getElementById('logTab').style.display = 'block';
|
||
renderLog();
|
||
}
|
||
});
|
||
});
|
||
|
||
// Экспорт
|
||
document.getElementById('exportBtn').addEventListener('click', exportToCSV);
|
||
|
||
// Тема
|
||
document.getElementById('themeToggle').addEventListener('click', () => {
|
||
const html = document.documentElement;
|
||
const current = html.getAttribute('data-theme');
|
||
const next = current === 'light' ? 'dark' : 'light';
|
||
html.setAttribute('data-theme', next);
|
||
localStorage.setItem('trackerTheme', next);
|
||
});
|
||
|
||
// Закрытие по клику вне модалки
|
||
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||
overlay.addEventListener('click', (e) => {
|
||
if (e.target === overlay) {
|
||
overlay.classList.remove('active');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Утилита
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// Тема
|
||
function updateTheme() {
|
||
const saved = localStorage.getItem('trackerTheme');
|
||
if (saved) {
|
||
document.documentElement.setAttribute('data-theme', saved);
|
||
}
|
||
}
|
||
|
||
// Старт
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|