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:
parent
933cdc6521
commit
86ebdf9e53
61
app.js
61
app.js
@ -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: '⏳',
|
||||
|
||||
@ -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>
|
||||
|
||||
26
styles.css
26
styles.css
@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user