Add progress updates per task

Each task gets an "updates" timeline. Tap "💬 Прогресс" in the
table row to open a wide modal that shows past entries
(author + timestamp + text) and a textarea for a new one.
Only the assignee or Абделазиз can add entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
sultan 2026-05-25 09:22:30 +05:00
parent 8652214f32
commit a20600138e
3 changed files with 170 additions and 2 deletions

70
app.js
View File

@ -289,6 +289,7 @@ function renderTable(tasks) {
<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>` : ''} ${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>`} ${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-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" data-act="delete" data-id="${t.id}" title="Удалить">✕</button>
</td> </td>
`; `;
@ -921,6 +922,70 @@ function initAuth() {
} }
} }
let progressTaskId = null;
function canEditProgress(task) {
return isDirector() || task.assignee === state.currentUser;
}
function openProgress(taskId) {
const task = getTasks().find(x => x.id === taskId);
if (!task) return;
progressTaskId = taskId;
$('#progressTaskTitle').textContent = task.title;
$('#progressTaskMeta').textContent = `${task.assignee || '—'} · дедлайн ${task.deadline || '—'}`;
renderProgressList(task);
const form = $('#progressForm');
const ta = $('#progressInput');
ta.value = '';
const allowed = canEditProgress(task);
form.hidden = !allowed;
$('#progressLocked').hidden = allowed;
$('#progressModal').hidden = false;
if (allowed) setTimeout(() => ta.focus(), 50);
}
function closeProgress() {
$('#progressModal').hidden = true;
progressTaskId = null;
}
function renderProgressList(task) {
const list = $('#progressList');
const updates = (task.updates || []).slice().reverse();
if (!updates.length) {
list.innerHTML = '<li class="progress-empty">Промежуточных результатов пока нет.</li>';
return;
}
list.innerHTML = updates.map(u => {
const dt = new Date(u.at);
const when = dt.toLocaleString('ru-RU', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' });
return `
<li class="progress-item">
<div class="progress-meta"><strong>${escapeHtml(u.by || '—')}</strong> · ${when}</div>
<div class="progress-text">${escapeHtml(u.text)}</div>
</li>
`;
}).join('');
}
function submitProgress(e) {
e.preventDefault();
if (progressTaskId == null) return;
const text = $('#progressInput').value.trim();
if (!text) return;
const tasks = getTasks();
const t = tasks.find(x => x.id === progressTaskId);
if (!t || !canEditProgress(t)) return;
t.updates = t.updates || [];
t.updates.push({ by: state.currentUser, at: new Date().toISOString(), text });
t.updated_at = new Date().toISOString();
save(tasks);
$('#progressInput').value = '';
renderProgressList(t);
renderAll();
}
function openProfile() { function openProfile() {
$('#profileName').textContent = state.currentUser || '—'; $('#profileName').textContent = state.currentUser || '—';
$('#profileRole').textContent = isDirector() ? 'Директор' : 'Сотрудник'; $('#profileRole').textContent = isDirector() ? 'Директор' : 'Сотрудник';
@ -1093,6 +1158,7 @@ function init() {
const tasks = getTasks(); const tasks = getTasks();
const t = tasks.find((x) => x.id === id); const t = tasks.find((x) => x.id === id);
if (!t) return; if (!t) 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 === 'reopen') t.status = 'in_progress'; if (act === 'reopen') t.status = 'in_progress';
@ -1106,6 +1172,10 @@ function init() {
renderAll(); renderAll();
}); });
$('#progressClose').addEventListener('click', closeProgress);
$('#progressBackdrop').addEventListener('click', closeProgress);
$('#progressForm').addEventListener('submit', submitProgress);
$('#tasksBody').addEventListener('change', (e) => { $('#tasksBody').addEventListener('change', (e) => {
const sel = e.target.closest('select.prio-select'); const sel = e.target.closest('select.prio-select');
if (!sel) return; if (!sel) return;

View File

@ -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=33" /> <link rel="stylesheet" href="styles.css?v=34" />
</head> </head>
<body> <body>
<header class="topbar"> <header class="topbar">
@ -68,6 +68,28 @@
</div> </div>
</header> </header>
<!-- Промежуточные результаты -->
<div id="progressModal" class="modal" hidden>
<div class="modal-backdrop" id="progressBackdrop"></div>
<div class="modal-card modal-wide" role="dialog" aria-modal="true" aria-labelledby="progressTaskTitle">
<button id="progressClose" class="modal-close" type="button" aria-label="Закрыть">×</button>
<h2 id="progressTaskTitle" class="modal-title">—</h2>
<div id="progressTaskMeta" class="progress-task-meta">—</div>
<h3 class="modal-sub">История</h3>
<ul id="progressList" class="progress-list">
<li class="progress-empty">Промежуточных результатов пока нет.</li>
</ul>
<h3 class="modal-sub">Добавить результат</h3>
<form id="progressForm" class="progress-form">
<textarea id="progressInput" rows="3" placeholder="Что сделано? Что осталось? Риски, блокеры..." required></textarea>
<button type="submit" class="btn btn-primary">+ Добавить</button>
</form>
<p id="progressLocked" class="hint" hidden>Только исполнитель или Абделазиз может добавлять записи.</p>
</div>
</div>
<!-- Профиль / смена пароля --> <!-- Профиль / смена пароля -->
<div id="profileModal" class="modal" hidden> <div id="profileModal" class="modal" hidden>
<div class="modal-backdrop" id="profileBackdrop"></div> <div class="modal-backdrop" id="profileBackdrop"></div>
@ -293,6 +315,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=30"></script> <script src="app.js?v=31"></script>
</body> </body>
</html> </html>

View File

@ -382,6 +382,82 @@ body:not([data-role]) .employee-only { display: none !important; }
} }
.profile-name { font-size: 16px; font-weight: 600; color: var(--ink); } .profile-name { font-size: 16px; font-weight: 600; color: var(--ink); }
.profile-role { font-size: 12px; color: var(--muted); padding: 4px 10px; background: var(--surface); border-radius: var(--r-pill); } .profile-role { font-size: 12px; color: var(--muted); padding: 4px 10px; background: var(--surface); border-radius: var(--r-pill); }
/* ---------- Progress modal ---------- */
.modal-wide { max-width: 560px; }
.progress-task-meta {
font-size: 13px;
color: var(--muted);
margin: -10px 0 18px 0;
}
.progress-list {
list-style: none;
margin: 0 0 20px 0;
padding: 0;
max-height: 280px;
overflow-y: auto;
border: 1px solid var(--hairline);
border-radius: var(--r-md);
background: var(--surface-2);
}
.progress-empty {
padding: 18px;
color: var(--muted);
font-size: 13px;
text-align: center;
}
.progress-item {
padding: 12px 16px;
border-top: 1px solid var(--hairline);
}
.progress-item:first-child { border-top: none; }
.progress-meta {
font-size: 12px;
color: var(--muted);
margin-bottom: 4px;
}
.progress-meta strong { color: var(--ink); font-weight: 600; }
.progress-text {
font-size: 14px;
color: var(--ink);
line-height: 1.45;
white-space: pre-wrap;
word-wrap: break-word;
}
.progress-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.progress-form textarea {
padding: 12px 14px;
font-size: 14px;
font-family: inherit;
border: 1px solid var(--border);
border-radius: var(--r-md);
background: var(--surface);
color: var(--ink);
outline: none;
resize: vertical;
min-height: 80px;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.progress-form textarea:focus {
border-color: var(--kt-blue);
box-shadow: 0 0 0 3px rgba(0,113,227,0.15);
}
.progress-form .btn-primary { align-self: flex-start; padding: 10px 18px; font-size: 14px; }
.upd-count {
display: inline-block;
background: var(--kt-blue);
color: #fff;
font-size: 10px;
font-weight: 700;
padding: 1px 6px;
border-radius: var(--r-pill);
margin-left: 4px;
vertical-align: 1px;
}
.who label { color: var(--muted); font-size: 12px; font-weight: 400; } .who label { color: var(--muted); font-size: 12px; font-weight: 400; }
.who select { .who select {
padding: 6px 28px 6px 12px; padding: 6px 28px 6px 12px;