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 = {
|
const STATUS_LABEL = {
|
||||||
new: 'Новая',
|
new: 'Новая',
|
||||||
in_progress: 'В работе',
|
in_progress: 'В работе',
|
||||||
|
pending_review: 'На утверждении',
|
||||||
done: 'Выполнена',
|
done: 'Выполнена',
|
||||||
overdue: 'Просрочена',
|
overdue: 'Просрочена',
|
||||||
};
|
};
|
||||||
@ -140,6 +141,7 @@ function computePriority(deadline) {
|
|||||||
|
|
||||||
function effectiveStatus(task) {
|
function effectiveStatus(task) {
|
||||||
if (task.status === 'done') return 'done';
|
if (task.status === 'done') return 'done';
|
||||||
|
if (task.status === 'pending_review') return 'pending_review';
|
||||||
if (task.deadline && task.deadline < todayISO()) return 'overdue';
|
if (task.deadline && task.deadline < todayISO()) return 'overdue';
|
||||||
return task.status;
|
return task.status;
|
||||||
}
|
}
|
||||||
@ -244,6 +246,35 @@ function renderDashboard(tasks) {
|
|||||||
if (!loadEl.children.length) loadEl.textContent = '—';
|
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) {
|
function renderTable(tasks) {
|
||||||
const body = $('#tasksBody');
|
const body = $('#tasksBody');
|
||||||
body.innerHTML = '';
|
body.innerHTML = '';
|
||||||
@ -287,10 +318,7 @@ function renderTable(tasks) {
|
|||||||
</td>
|
</td>
|
||||||
<td><span class="badge status-${st}">${STATUS_LABEL[st]}</span></td>
|
<td><span class="badge status-${st}">${STATUS_LABEL[st]}</span></td>
|
||||||
<td class="actions-cell">
|
<td class="actions-cell">
|
||||||
${st !== 'done' && t.status === 'new' ? `<button class="btn btn-sm btn-primary" data-act="start" data-id="${t.id}">▶ В работу</button>` : ''}
|
${renderRowActions(t, st)}
|
||||||
${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>
|
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
body.appendChild(tr);
|
body.appendChild(tr);
|
||||||
@ -1207,6 +1235,25 @@ function init() {
|
|||||||
if (act === 'progress') { openProgress(t.id); return; }
|
if (act === 'progress') { openProgress(t.id); return; }
|
||||||
if (act === 'start') t.status = 'in_progress';
|
if (act === 'start') t.status = 'in_progress';
|
||||||
if (act === 'done') t.status = 'done';
|
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 === 'reopen') t.status = 'in_progress';
|
||||||
if (act === 'delete') {
|
if (act === 'delete') {
|
||||||
if (!isDirector()) return;
|
if (!isDirector()) return;
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 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="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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
@ -249,6 +249,7 @@
|
|||||||
<option value="">Все статусы</option>
|
<option value="">Все статусы</option>
|
||||||
<option value="new">Новая</option>
|
<option value="new">Новая</option>
|
||||||
<option value="in_progress">В работе</option>
|
<option value="in_progress">В работе</option>
|
||||||
|
<option value="pending_review">На утверждении</option>
|
||||||
<option value="overdue">Просрочена</option>
|
<option value="overdue">Просрочена</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="fDirection"><option value="">Все направления</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/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/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="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>
|
</body>
|
||||||
</html>
|
</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-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-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-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; }
|
td.actions-cell { white-space: nowrap; display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user