Route task completion through director approval
Employees clicking "Готово" now move the task to status="pending_review" instead of "done". The task stays in the active table with a purple "На утверждении" badge until Абделазиз acts on it: - ✓ Утвердить → status="done" → goes to archive - ✗ Отклонить → status="in_progress" → back to work Employees can revoke their own submission while it is pending. Абделазиз's own "Готово" still completes immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9de998a80c
commit
0429d31f0c
55
app.js
55
app.js
@ -90,6 +90,7 @@ const DIRECTIONS = [
|
||||
const STATUS_LABEL = {
|
||||
new: 'Новая',
|
||||
in_progress: 'В работе',
|
||||
pending_review: 'На утверждении',
|
||||
done: 'Выполнена',
|
||||
overdue: 'Просрочена',
|
||||
};
|
||||
@ -140,6 +141,7 @@ function computePriority(deadline) {
|
||||
|
||||
function effectiveStatus(task) {
|
||||
if (task.status === 'done') return 'done';
|
||||
if (task.status === 'pending_review') return 'pending_review';
|
||||
if (task.deadline && task.deadline < todayISO()) return 'overdue';
|
||||
return task.status;
|
||||
}
|
||||
@ -244,6 +246,35 @@ function renderDashboard(tasks) {
|
||||
if (!loadEl.children.length) loadEl.textContent = '—';
|
||||
}
|
||||
|
||||
function renderRowActions(t, st) {
|
||||
const id = t.id;
|
||||
const isOwner = t.assignee === state.currentUser;
|
||||
const dir = isDirector();
|
||||
const progBtn = `<button class="btn btn-sm btn-icon btn-ghost" data-act="progress" data-id="${id}" title="Промежуточные результаты">💬${(t.updates && t.updates.length) ? `<span class="upd-count">${t.updates.length}</span>` : ''}</button>`;
|
||||
const delBtn = `<button class="btn btn-sm btn-icon btn-danger director-only" data-act="delete" data-id="${id}" title="Удалить">✕</button>`;
|
||||
|
||||
if (st === 'pending_review') {
|
||||
let main = '';
|
||||
if (dir) {
|
||||
main = `
|
||||
<button class="btn btn-sm btn-success" data-act="approve" data-id="${id}">✓ Утвердить</button>
|
||||
<button class="btn btn-sm btn-ghost" data-act="reject" data-id="${id}">✗ Отклонить</button>
|
||||
`;
|
||||
} else if (isOwner) {
|
||||
main = `<button class="btn btn-sm btn-ghost" data-act="revoke" data-id="${id}">↺ Отозвать</button>`;
|
||||
} else {
|
||||
main = `<span class="hint" style="margin-right:auto;">Ожидает решения Абделазиза</span>`;
|
||||
}
|
||||
return `${main}${progBtn}${delBtn}`;
|
||||
}
|
||||
|
||||
const startBtn = (t.status === 'new') ? `<button class="btn btn-sm btn-primary" data-act="start" data-id="${id}">▶ В работу</button>` : '';
|
||||
const doneAct = dir ? 'done' : 'submit_review';
|
||||
const doneLbl = dir ? '✓ Готово' : '✓ На утверждение';
|
||||
const doneBtn = `<button class="btn btn-sm btn-success" data-act="${doneAct}" data-id="${id}">${doneLbl}</button>`;
|
||||
return `${startBtn}${doneBtn}${progBtn}${delBtn}`;
|
||||
}
|
||||
|
||||
function renderTable(tasks) {
|
||||
const body = $('#tasksBody');
|
||||
body.innerHTML = '';
|
||||
@ -287,10 +318,7 @@ function renderTable(tasks) {
|
||||
</td>
|
||||
<td><span class="badge status-${st}">${STATUS_LABEL[st]}</span></td>
|
||||
<td class="actions-cell">
|
||||
${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 director-only" data-act="delete" data-id="${t.id}" title="Удалить">✕</button>
|
||||
${renderRowActions(t, st)}
|
||||
</td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
@ -1207,6 +1235,25 @@ function init() {
|
||||
if (act === 'progress') { openProgress(t.id); return; }
|
||||
if (act === 'start') t.status = 'in_progress';
|
||||
if (act === 'done') t.status = 'done';
|
||||
if (act === 'submit_review') {
|
||||
if (t.assignee !== state.currentUser && !isDirector()) return;
|
||||
t.status = 'pending_review';
|
||||
showToast(`Поручение «${t.title}» отправлено Абделазизу на утверждение`);
|
||||
}
|
||||
if (act === 'approve') {
|
||||
if (!isDirector()) return;
|
||||
t.status = 'done';
|
||||
showToast(`Утверждено: «${t.title}»`);
|
||||
}
|
||||
if (act === 'reject') {
|
||||
if (!isDirector()) return;
|
||||
t.status = 'in_progress';
|
||||
showToast(`Отклонено: «${t.title}» возвращено в работу`);
|
||||
}
|
||||
if (act === 'revoke') {
|
||||
if (t.assignee !== state.currentUser && !isDirector()) return;
|
||||
t.status = 'in_progress';
|
||||
}
|
||||
if (act === 'reopen') t.status = 'in_progress';
|
||||
if (act === 'delete') {
|
||||
if (!isDirector()) return;
|
||||
|
||||
@ -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=37" />
|
||||
<link rel="stylesheet" href="styles.css?v=38" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
@ -249,6 +249,7 @@
|
||||
<option value="">Все статусы</option>
|
||||
<option value="new">Новая</option>
|
||||
<option value="in_progress">В работе</option>
|
||||
<option value="pending_review">На утверждении</option>
|
||||
<option value="overdue">Просрочена</option>
|
||||
</select>
|
||||
<select id="fDirection"><option value="">Все направления</option></select>
|
||||
@ -355,6 +356,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=34"></script>
|
||||
<script src="app.js?v=35"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -790,6 +790,7 @@ td.title { max-width: 380px; }
|
||||
.badge.status-in_progress { background: rgba(255, 149, 0, 0.14); color: var(--warn); }
|
||||
.badge.status-done { background: rgba(52, 199, 89, 0.14); color: var(--ok); }
|
||||
.badge.status-overdue { background: rgba(255, 59, 48, 0.12); color: var(--danger); }
|
||||
.badge.status-pending_review { background: rgba(175, 82, 222, 0.14); color: #AF52DE; }
|
||||
|
||||
td.actions-cell { white-space: nowrap; display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user