From a87607191179a7fb23c00a2d38312b79ac26c27a Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 6 Oct 2026 16:21:41 +0300 Subject: [PATCH] feat(feedback): improve feedback workflow --- public/feedback.html | 85 ++++++++++++++++++------------------------- public/js/feedback.js | 48 +++++++++++++++--------- server.js | 34 ++++++++++++++++- 3 files changed, 99 insertions(+), 68 deletions(-) diff --git a/public/feedback.html b/public/feedback.html index 729a528..538faba 100644 --- a/public/feedback.html +++ b/public/feedback.html @@ -7,65 +7,50 @@ -
+
-
-
-

Обратная связь

-
Отзывы тьютора по итогам занятий
+
+

Обратная связь

+

Отзывы тьютора по итогам занятий

+
+ +
+
+
+
+
+
+
+
- -
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
+
+
+ +
+ +
diff --git a/public/js/feedback.js b/public/js/feedback.js index 464c283..be7c867 100644 --- a/public/js/feedback.js +++ b/public/js/feedback.js @@ -25,24 +25,26 @@ function fbItemHtml(f) { const text = (f.text||'').trim(); - const short = text.length > 100 ? text.slice(0,97)+'…' : text; const del = f.deleted_at ? ' style="opacity:.5"' : ''; - return `
-
${fmtDate(f.feedback_date)}
-
${escapeHtml(f.topic||'')}
-
${escapeHtml(f.resident||'')}
-
${fmtDay(f.feedback_date)}
-
${escapeHtml(f.group_name||'')}
-
${escapeHtml(short)}
-
+ const day = fmtDay(f.feedback_date); + return ``; } @@ -175,7 +177,7 @@ async function fetchShorts(q) { if (!q || q.length < 2) { shortsListCache = []; renderShorts([]); return; } try { - const r = await fetch(`${API}/api/shorts?search=${encodeURIComponent(q)}&limit=50&deleted=0`, { headers: hdr() }); + const r = await fetch(`${API}/api/shorts?search=${encodeURIComponent(q)}&limit=50&deleted=0&used=0`, { headers: hdr() }); if (r.ok) { const j = await r.json(); shortsListCache = j.items || []; renderShorts(shortsListCache); @@ -183,6 +185,18 @@ } catch (e) {} } + async function loadLastPastReview() { + const resident = document.getElementById('fbResident').value.trim(); + if (!resident) return; + try { + const r = await fetch(`${API}/api/feedbacks/last?resident=${encodeURIComponent(resident)}`, { headers: hdr() }); + if (r.ok) { + const row = await r.json(); + if (row && row.text) document.getElementById('fbPastReview').value = row.text; + } + } catch (e) {} + } + function renderShorts(items) { const wrap = document.getElementById('fbShortsWrap'); const list = document.getElementById('fbShortsList'); @@ -335,6 +349,7 @@ document.getElementById('fbResident').addEventListener('change', applyResidentGroup); document.getElementById('fbPastCheck').addEventListener('change', e => { document.getElementById('fbPastReviewWrap').style.display = e.target.checked ? '' : 'none'; + if (e.target.checked) loadLastPastReview(); }); document.getElementById('fbText').addEventListener('input', () => { document.getElementById('fbTextHint').textContent = `${document.getElementById('fbText').value.length} / 5000`; @@ -370,11 +385,10 @@ return; } }); - ['fbSearch','fbGroupFilter','fbStatusFilter','fbDateFrom','fbDateTo'].forEach(id => { + ['fbSearch','fbGroupFilter','fbStatusFilter','fbDateFrom','fbDateTo','fbDeletedToggle'].forEach(id => { document.getElementById(id).addEventListener('change', () => { page = 1; loadFeedbacks(); }); if (id === 'fbSearch') document.getElementById(id).addEventListener('input', () => { clearTimeout(searchTimer); searchTimer = setTimeout(() => { page = 1; loadFeedbacks(); }, 350); }); }); - document.getElementById('fbDeletedToggle').addEventListener('change', () => { page = 1; loadFeedbacks(); }); if (window.lucide) lucide.createIcons(); }); diff --git a/server.js b/server.js index e556d0e..c86a250 100644 --- a/server.js +++ b/server.js @@ -2750,8 +2750,11 @@ app.get('/api/shorts', requireAuth, async (req, res) => { const offset = Math.max(parseInt(req.query.offset, 10) || 0, 0); const showDeleted = req.query.deleted === '1' || req.query.deleted === 'true'; const search = req.query.search ? String(req.query.search).trim() : ''; + const usedFilter = req.query.used; const params = []; let where = showDeleted ? 'WHERE sm.deleted_at IS NOT NULL' : 'WHERE sm.deleted_at IS NULL'; + if (usedFilter === '0' || usedFilter === 'false') where += ' AND sm.used = false'; + else if (usedFilter === '1' || usedFilter === 'true') where += ' AND sm.used = true'; if (search) { params.push(`%${search}%`); where += ` AND (sm.resident ILIKE $${params.length} OR sm.topic ILIKE $${params.length} OR sm.message ILIKE $${params.length})`; @@ -2935,6 +2938,35 @@ function parseFeedbackDate(s) { return str; } +app.get('/api/feedbacks/last', requireAuth, async (req, res) => { + try { + const resident = typeof req.query.resident === 'string' ? req.query.resident.trim() : ''; + if (!resident) return res.status(400).json({ error: 'Нужен резидент' }); + const key = ['feedbacks:last:', scopeKey(req.user), resident.toLowerCase()].join(':'); + const payload = await cacheWrap(key, FEEDBACK_LIST_TTL_MS, async () => { + const bs = branchScope(req.user); + if (bs.ids && !bs.ids.length) return null; + const params = [`%${resident}%`]; + let cond = 'f.deleted_at IS NULL AND f.resident ILIKE $1'; + if (bs.ids) { params.push(bs.ids); cond += ` AND f.branch_id = ANY($${params.length}::int[])`; } + const { rows } = await pool.query( + `SELECT f.id, f.feedback_date, f.group_id, f.resident, f.topic, f.past_review, f.text, f.ai_status, f.sent_at, f.branch_id, f.deleted_at, g.name AS group_name + FROM feedbacks f + LEFT JOIN groups g ON g.id = f.group_id + WHERE ${cond} + ORDER BY f.feedback_date DESC NULLS LAST, f.id DESC + LIMIT 1`, + params + ); + return rows[0] || null; + }); + res.json(payload); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Internal error' }); + } +}); + app.get('/api/feedbacks', requireAuth, async (req, res) => { try { const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100); @@ -5690,7 +5722,7 @@ app.get('/api/students/names-with-groups', requireAuth, async (req, res) => { `SELECT DISTINCT ON (e.student_name) e.student_name AS name, e.group_id, g.name AS group_name FROM entries e JOIN groups g ON g.id = e.group_id - WHERE e.student_name IS NOT NULL AND e.student_name <> ''${bw.where} + WHERE e.student_name IS NOT NULL AND e.student_name <> '' AND e.deleted_at IS NULL${bw.where} ORDER BY e.student_name, e.created_at DESC NULLS LAST, e.id DESC`, bw.params );