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:
parent
2933fe6ccf
commit
32378c342c
52
app.js
52
app.js
@ -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);
|
||||
|
||||
32
index.html
32
index.html
@ -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>
|
||||
|
||||
26
styles.css
26
styles.css
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user