Bold names and clickable task chips in notifications

In the notification feed, employee names are bolded and the
task title (in «…») becomes a pill-shaped button. Clicking it
closes the modal, navigates to #/tasks, expands the archive
section if the task lives there, scrolls the row into view,
and runs a brief highlight pulse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
sultan 2026-05-25 09:53:36 +05:00
parent 933cdc6521
commit 86ebdf9e53
3 changed files with 85 additions and 6 deletions

61
app.js
View File

@ -336,6 +336,7 @@ function renderTable(tasks) {
const st = effectiveStatus(t);
const tr = document.createElement('tr');
tr.className = `prio-${t.priority} ${st === 'overdue' ? 'overdue' : ''} ${st === 'done' ? 'done' : ''}`;
tr.dataset.taskId = t.id;
tr.innerHTML = `
<td class="title" title="${escapeHtml(t.description || '')}">${escapeHtml(t.title)}</td>
@ -387,7 +388,7 @@ function renderArchive(tasks) {
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">
<tr class="archived" data-task-id="${t.id}">
<td>${escapeHtml(t.title)}</td>
<td>${escapeHtml(t.assignee || '')}</td>
<td>${escapeHtml(t.direction || '')}</td>
@ -976,6 +977,7 @@ function applyRoute() {
closeMenu();
renderAll();
if (view === 'home') startHomeWidgets();
if (view === 'tasks') applyHighlight();
}
// ---------- auth ----------
@ -1198,20 +1200,73 @@ function renderNotifList() {
const when = dt.toLocaleString('ru-RU', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' });
const cls = `notif-item notif-${n.type}${n.read ? '' : ' is-unread'}`;
const icon = NOTIF_ICONS[n.type] || '•';
const text = formatNotifText(n.text, n.taskId);
return `
<li class="${cls}">
<span class="notif-icon">${icon}</span>
<div class="notif-body">
<div class="notif-text">${escapeHtml(n.text)}</div>
<div class="notif-text">${text}</div>
<div class="notif-when">${when}</div>
</div>
</li>
`;
}).join('');
// mark as read on open
list.querySelectorAll('.notif-task-link').forEach(el => {
el.addEventListener('click', () => {
const tid = Number(el.getAttribute('data-task-id'));
gotoTask(tid);
});
});
setTimeout(() => { markAllRead(); }, 800);
}
function formatNotifText(rawText, taskId) {
let html = escapeHtml(rawText);
EMPLOYEES.forEach(name => {
const re = new RegExp(`\\b(${name.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')})\\b`, 'g');
html = html.replace(re, '<strong>$1</strong>');
});
if (taskId != null) {
html = html.replace(/«([^»]+)»/g, (_, title) =>
`<button type="button" class="notif-task-link" data-task-id="${taskId}">«${title}»</button>`
);
}
return html;
}
function gotoTask(taskId) {
const t = getTasks().find(x => x.id === taskId);
closeNotifs();
if (!t) { showToast('Поручение не найдено (возможно, удалено).'); return; }
state.highlightTaskId = taskId;
if (effectiveStatus(t) === 'done') state.highlightArchive = true;
if (location.hash !== '#/tasks') location.hash = '#/tasks';
else applyHighlight();
}
function applyHighlight() {
if (state.highlightArchive) {
const box = $('#archiveBox');
if (box && box.hidden) {
box.hidden = false;
const btn = $('#archiveToggle');
if (btn) { btn.textContent = 'Скрыть'; btn.setAttribute('aria-expanded', 'true'); }
}
}
const id = state.highlightTaskId;
if (id == null) return;
setTimeout(() => {
const row = document.querySelector(`tr[data-task-id="${id}"]`);
if (row) {
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
row.classList.add('row-highlight');
setTimeout(() => row.classList.remove('row-highlight'), 2400);
}
state.highlightTaskId = null;
state.highlightArchive = false;
}, 80);
}
const NOTIF_ICONS = {
new_task: '📋',
review: '⏳',

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=40" />
<link rel="stylesheet" href="styles.css?v=41" />
</head>
<body>
<header class="topbar">
@ -380,6 +380,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=36"></script>
<script src="app.js?v=37"></script>
</body>
</html>

View File

@ -306,8 +306,32 @@ body {
border-radius: 50%;
}
.notif-body { min-width: 0; }
.notif-text { font-size: 14px; color: var(--ink); line-height: 1.4; }
.notif-text { font-size: 14px; color: var(--ink); line-height: 1.45; }
.notif-text strong { font-weight: 600; color: var(--ink); }
.notif-when { font-size: 11px; color: var(--muted); margin-top: 4px; }
.notif-task-link {
display: inline;
border: none;
background: rgba(0, 113, 227, 0.10);
color: var(--kt-blue);
font: inherit;
font-weight: 500;
padding: 2px 8px;
margin: 0 1px;
border-radius: var(--r-pill);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.notif-task-link:hover { background: var(--kt-blue); color: #fff; }
.notif-task-link:focus-visible { outline: 2px solid var(--kt-blue); outline-offset: 2px; }
@keyframes row-highlight-pulse {
0% { background-color: rgba(0, 113, 227, 0.25); }
100% { background-color: transparent; }
}
tbody tr.row-highlight td {
animation: row-highlight-pulse 2.4s ease-out;
}
body[data-role="employee"] .director-only { display: none !important; }
body[data-role="director"] .employee-only { display: none !important; }
body:not([data-role]) .employee-only { display: none !important; }