sync2 vibe

This commit is contained in:
dv 2026-03-23 22:38:53 +03:00
parent fc2b7f762a
commit 232a6b4238
3 changed files with 76 additions and 4 deletions

View file

@ -84,6 +84,17 @@ function startSSE() {
refreshDetailIfOpen(tid); refreshDetailIfOpen(tid);
}); });
sseSource.addEventListener('intra_addfield',e=>{
const d=JSON.parse(e.data);
const item=allData.find((x)=>String(x.task.id)===String(d.task_id));
if(item) item.task._addfield_status=d.value||null;
if(d.by!==currentUser?.username){
showToast(`🔗 ${d.by}: служ. статус → «${d.value||'—'}» #${taskNumFromId(d.task_id)}`,'info');
}
renderTable();
refreshDetailIfOpen(d.task_id);
});
sseSource.addEventListener('tasks_updated',e=>{ sseSource.addEventListener('tasks_updated',e=>{
const d=JSON.parse(e.data); const d=JSON.parse(e.data);
const incomingVersion=Number(d.cache_version||0); const incomingVersion=Number(d.cache_version||0);

View file

@ -455,9 +455,8 @@ function intraStatusBadge(task) {
else if (lc.includes('ожидан') || lc.includes('hold')) cls = 'sb-intra-hold'; else if (lc.includes('ожидан') || lc.includes('hold')) cls = 'sb-intra-hold';
return `<span class="status-badge ${cls}"><span class="dot"></span>${esc(name)}</span>`; return `<span class="status-badge ${cls}"><span class="dot"></span>${esc(name)}</span>`;
} }
// ── IntraDesk additional field status badge ─────────────────────── // ── IntraDesk additional field — badge helper ─────────────────────
function intraAddfieldBadge(task) { function _addfieldBadgeHtml(val) {
const val = task._addfield_status;
if (!val) return '<span style="color:var(--text3);font-size:11px;">—</span>'; if (!val) return '<span style="color:var(--text3);font-size:11px;">—</span>';
const match = (typeof MARKS_CONFIG !== 'undefined' ? MARKS_CONFIG : []) const match = (typeof MARKS_CONFIG !== 'undefined' ? MARKS_CONFIG : [])
.find((m) => m.label && m.label.toLowerCase() === val.toLowerCase()); .find((m) => m.label && m.label.toLowerCase() === val.toLowerCase());
@ -467,6 +466,36 @@ function intraAddfieldBadge(task) {
} }
return `<span class="status-badge sb-intra-other"><span class="dot"></span>${esc(val)}</span>`; return `<span class="status-badge sb-intra-other"><span class="dot"></span>${esc(val)}</span>`;
} }
// ── IntraDesk additional field — interactive picker ───────────────
function intraAddfieldPickerHtml(task) {
const tid = String(task.id);
const val = task._addfield_status || null;
const cfg = typeof MARKS_CONFIG !== 'undefined' ? MARKS_CONFIG : [];
const items = cfg.map((m) => `
<div class="isp-item ${val && val.toLowerCase()===m.label.toLowerCase()?'isp-active':''}"
onclick="setIntraAddfield('${esc(tid)}','${esc(m.label)}',event)">
<span class="isp-dot" style="background:${m.ispColor||m.colorDot||'#1677ff'};"></span>${esc(m.label)}
</div>`).join('');
const clearItem = `<div class="isp-item${!val?' isp-active':''}" onclick="setIntraAddfield('${esc(tid)}',null,event)">
<span class="isp-dot" style="background:var(--text3);"></span><span style="color:var(--text3);"> очистить</span>
</div>`;
return `<div class="inline-status-picker" onmouseenter="ispShow(this)" onmouseleave="ispHide(this)">
${_addfieldBadgeHtml(val)}
<div class="isp-dropdown">
<div class="isp-dropdown-inner">${items}${clearItem}</div>
</div>
</div>`;
}
async function setIntraAddfield(taskId, value, e) {
if (e) e.stopPropagation();
const r = await apiPost(`/api/tasks/${taskId}/intra-addfield`, { value: value || null });
if (!r?.ok) { showToast(`${r?.error || 'Ошибка'}`, 'warn'); return; }
// Обновляем кеш локально до следующего SSE
const item = allData.find((d) => String(d.task.id) === String(taskId));
if (item) { item.task._addfield_status = value || null; renderTable(); }
}
// Who answered — ring class for avatar // Who answered — ring class for avatar
function whoAnsweredRing(lc){ function whoAnsweredRing(lc){
if(!lc) return 'ring-none'; if(!lc) return 'ring-none';
@ -756,7 +785,7 @@ function renderTable(keepPage){
${sc('num') ?`<td><span class="task-num">#${num}</span>${unreadDot}</td>`:''} ${sc('num') ?`<td><span class="task-num">#${num}</span>${unreadDot}</td>`:''}
${sc('title') ?`<td><div class="task-title-main" title="${esc(task.name)}">${esc(task.name)}</div>${orgName?`<div class="task-title-sub">${esc(orgName)}</div>`:''}</td>`:''} ${sc('title') ?`<td><div class="task-title-main" title="${esc(task.name)}">${esc(task.name)}</div>${orgName?`<div class="task-title-sub">${esc(orgName)}</div>`:''}</td>`:''}
${sc('intrastatus')?`<td>${intraStatusBadge(task)}</td>`:''} ${sc('intrastatus')?`<td>${intraStatusBadge(task)}</td>`:''}
${sc('servstatus') ?`<td>${intraAddfieldBadge(task)}</td>`:''} ${sc('servstatus') ?`<td onclick="event.stopPropagation()">${intraAddfieldPickerHtml(task)}</td>`:''}
${sc('status') ?`<td onclick="event.stopPropagation()">${statusPickerHtml(tid)}</td>`:''} ${sc('status') ?`<td onclick="event.stopPropagation()">${statusPickerHtml(tid)}</td>`:''}
${sc('client') ?`<td><div class="client-info"> ${sc('client') ?`<td><div class="client-info">
<div class="client-avatar ${whoAnsweredRing(lastComment)}" style="background:${avatarColor(authorName)};color:#fff;">${avatarInitials(authorName)}</div> <div class="client-avatar ${whoAnsweredRing(lastComment)}" style="background:${avatarColor(authorName)};color:#fff;">${avatarInitials(authorName)}</div>

View file

@ -790,6 +790,38 @@ app.post('/api/marks', auth, (req, res) => {
res.json({ ok: true }); res.json({ ok: true });
}); });
// ── IntraDesk additional field (direct set) ───────────────────────
app.post('/api/tasks/:taskId/intra-addfield', auth, async (req, res) => {
const taskId = String(req.params.taskId || '').trim();
const { value } = req.body || {};
const statusLabel = value ? String(value).trim() : null;
if (!taskId) return res.status(400).json({ error: 'task_id required' });
const s = getSettings();
if (!s.api_key) return res.status(400).json({ error: 'API ключ не задан' });
const cacheRow = db.prepare('SELECT data FROM tasks_cache WHERE id=1').get();
const tasks = JSON.parse(cacheRow?.data || '[]');
const task = tasks.find((t) => String(t.id) === taskId);
if (!task) return res.status(404).json({ error: 'Задача не найдена в кеше' });
try {
await refreshService.updateIntraAdditionalField(task.tasknumber, statusLabel, s.api_key);
} catch (e) {
console.error('[AddField] IntraDesk update error:', e.message);
return res.status(502).json({ error: `Ошибка IntraDesk: ${e.message}` });
}
// Обновляем значение в локальном кеше немедленно
task._addfield_status = statusLabel;
const now = new Date().toISOString();
db.prepare('UPDATE tasks_cache SET data=?, updated_at=? WHERE id=1')
.run(JSON.stringify(tasks), now);
sseBroadcast('intra_addfield', { task_id: taskId, value: statusLabel, by: req.user.username });
res.json({ ok: true, value: statusLabel });
});
// ── Notes ───────────────────────────────────────────────────────── // ── Notes ─────────────────────────────────────────────────────────
app.get('/api/notes', auth, (req, res) => { app.get('/api/notes', auth, (req, res) => {
const r = {}; const r = {};