Restrict delete to Абделазиз, move done tasks to archive

- The delete button is now director-only via CSS + JS guard.
- The main tasks table only shows active tasks (not done).
- A collapsible "Архив" section lists completed tasks (employees
  see their own; Абделазиз sees everyone's). Each archived row
  has "↺ Вернуть" and progress access; delete is director-only.
- Department charts are now director-only too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
sultan 2026-05-25 09:27:54 +05:00
parent 2933fe6ccf
commit 32378c342c
3 changed files with 100 additions and 10 deletions

52
app.js
View File

@ -249,7 +249,7 @@ function renderTable(tasks) {
body.innerHTML = '';
const f = state.filters;
let visible = tasks.slice();
let visible = tasks.filter((t) => effectiveStatus(t) !== 'done');
// Сотрудник видит только свои по умолчанию (если фильтр не задан явно).
if (!isDirector() && !f.assignee) {
@ -258,7 +258,7 @@ function renderTable(tasks) {
if (f.assignee) visible = visible.filter((t) => t.assignee === f.assignee);
if (f.direction) visible = visible.filter((t) => t.direction === f.direction);
if (f.priority) visible = visible.filter((t) => t.priority === f.priority);
if (f.status) visible = visible.filter((t) => effectiveStatus(t) === f.status);
if (f.status && f.status !== 'done') visible = visible.filter((t) => effectiveStatus(t) === f.status);
if (f.deadline) visible = visible.filter((t) => t.deadline === f.deadline);
visible.sort((a, b) => {
@ -290,7 +290,7 @@ function renderTable(tasks) {
${st !== 'done' && t.status === 'new' ? `<button class="btn btn-sm btn-primary" data-act="start" data-id="${t.id}">▶ В работу</button>` : ''}
${st !== 'done' ? `<button class="btn btn-sm btn-success" data-act="done" data-id="${t.id}">✓ Готово</button>` : `<button class="btn btn-sm btn-ghost" data-act="reopen" data-id="${t.id}">↺ Вернуть</button>`}
<button class="btn btn-sm btn-icon btn-ghost" data-act="progress" data-id="${t.id}" title="Промежуточные результаты">💬${(t.updates && t.updates.length) ? `<span class="upd-count">${t.updates.length}</span>` : ''}</button>
<button class="btn btn-sm btn-icon btn-danger" data-act="delete" data-id="${t.id}" title="Удалить">✕</button>
<button class="btn btn-sm btn-icon btn-danger director-only" data-act="delete" data-id="${t.id}" title="Удалить">✕</button>
</td>
`;
body.appendChild(tr);
@ -306,10 +306,40 @@ function renderAll() {
if (state.view === 'analytics') {
renderDashboard(tasks);
renderTable(tasks);
renderArchive(tasks);
renderWorkload(tasks);
}
}
function renderArchive(tasks) {
const body = $('#archiveBody');
const empty = $('#archiveEmpty');
const countEl = $('#archiveCount');
if (!body) return;
let archived = tasks.filter((t) => effectiveStatus(t) === 'done');
if (!isDirector()) archived = archived.filter((t) => t.assignee === state.currentUser);
archived.sort((a, b) => (b.updated_at || '').localeCompare(a.updated_at || ''));
countEl.textContent = archived.length;
empty.hidden = archived.length > 0;
body.innerHTML = archived.map((t) => {
const completed = t.updated_at ? new Date(t.updated_at).toLocaleDateString('ru-RU', { day: '2-digit', month: 'short', year: 'numeric' }) : '—';
return `
<tr class="archived">
<td>${escapeHtml(t.title)}</td>
<td>${escapeHtml(t.assignee || '')}</td>
<td>${escapeHtml(t.direction || '')}</td>
<td>${t.deadline || ''}</td>
<td>${completed}</td>
<td class="actions-cell">
<button class="btn btn-sm btn-ghost" data-act="reopen" data-id="${t.id}">↺ Вернуть</button>
<button class="btn btn-sm btn-icon btn-ghost" data-act="progress" data-id="${t.id}" title="Промежуточные результаты">💬${(t.updates && t.updates.length) ? `<span class="upd-count">${t.updates.length}</span>` : ''}</button>
<button class="btn btn-sm btn-icon btn-danger director-only" data-act="delete" data-id="${t.id}" title="Удалить">✕</button>
</td>
</tr>
`;
}).join('');
}
// ---------- workload view (графики) ----------
const charts = {};
@ -1149,8 +1179,8 @@ function init() {
renderAll();
});
// Делегирование действий в таблице
$('#tasksBody').addEventListener('click', (e) => {
// Делегирование действий в таблицах (активные + архив)
const onActionClick = (e) => {
const btn = e.target.closest('button[data-act]');
if (!btn) return;
const id = Number(btn.dataset.id);
@ -1163,6 +1193,7 @@ function init() {
if (act === 'done') t.status = 'done';
if (act === 'reopen') t.status = 'in_progress';
if (act === 'delete') {
if (!isDirector()) return;
if (!confirm('Удалить поручение?')) return;
const idx = tasks.indexOf(t);
tasks.splice(idx, 1);
@ -1170,6 +1201,17 @@ function init() {
t && (t.updated_at = new Date().toISOString());
save(tasks);
renderAll();
};
$('#tasksBody').addEventListener('click', onActionClick);
$('#archiveBody').addEventListener('click', onActionClick);
$('#archiveToggle').addEventListener('click', () => {
const box = $('#archiveBox');
const btn = $('#archiveToggle');
const isHidden = box.hidden;
box.hidden = !isHidden;
btn.textContent = isHidden ? 'Скрыть' : 'Показать';
btn.setAttribute('aria-expanded', String(isHidden));
});
$('#progressClose').addEventListener('click', closeProgress);

View File

@ -11,7 +11,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css">
<link rel="stylesheet" href="styles.css?v=35" />
<link rel="stylesheet" href="styles.css?v=36" />
</head>
<body>
<header class="topbar">
@ -251,7 +251,6 @@
<option value="">Все статусы</option>
<option value="new">Новая</option>
<option value="in_progress">В работе</option>
<option value="done">Выполнена</option>
<option value="overdue">Просрочена</option>
</select>
<select id="fDirection"><option value="">Все направления</option></select>
@ -282,16 +281,39 @@
</section>
<section class="card">
<div class="archive-head">
<h2>Архив <span class="archive-count" id="archiveCount">0</span></h2>
<button id="archiveToggle" class="btn btn-ghost" type="button" aria-expanded="false">Показать</button>
</div>
<div id="archiveBox" hidden>
<table id="archiveTable">
<thead>
<tr>
<th>Заголовок</th>
<th>Исполнитель</th>
<th>Направление</th>
<th>Дедлайн</th>
<th>Завершено</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="archiveBody"></tbody>
</table>
<p id="archiveEmpty" class="hint" hidden>Архив пуст.</p>
</div>
</section>
<section class="card director-only">
<h2>Задачи по сотрудникам (по статусу)</h2>
<div class="chart-wrap"><canvas id="chartStatus"></canvas></div>
</section>
<section class="card">
<section class="card director-only">
<h2>Задачи по сотрудникам (по приоритету)</h2>
<div class="chart-wrap"><canvas id="chartPriority"></canvas></div>
</section>
<div class="charts-row">
<div class="charts-row director-only">
<section class="card">
<h2>Распределение по дедлайнам</h2>
<div class="chart-wrap small"><canvas id="chartDeadlines"></canvas></div>
@ -315,6 +337,6 @@
<script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/l10n/ru.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script src="app.js?v=32"></script>
<script src="app.js?v=33"></script>
</body>
</html>

View File

@ -459,6 +459,32 @@ body:not([data-role]) .employee-only { display: none !important; }
vertical-align: 2px;
line-height: 1;
}
/* ---------- Archive ---------- */
.archive-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.archive-head h2 { margin: 0; display: flex; align-items: center; gap: 10px; }
.archive-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 26px;
height: 22px;
padding: 0 8px;
font-size: 12px;
font-weight: 600;
color: var(--muted);
background: var(--surface-2);
border-radius: var(--r-pill);
}
#archiveTable tbody tr.archived td { color: var(--muted); }
#archiveTable tbody tr.archived td:first-child { color: var(--ink-2); }
.who label { color: var(--muted); font-size: 12px; font-weight: 400; }
.who select {
padding: 6px 28px 6px 12px;