-
+
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
);
+ ${escapeHtml(f.group_name||'')}
+ ${fmtDate(f.feedback_date)}${day ? ` · ${day}` : ''}
+ ${f.deleted_at ? 'Удалён' : ''}
${aiBadge(f.ai_status)}
${f.sent_at ? 'Отправлено' : ''}
- ${f.ai_status === 'error' ? `` : ''}
- ${f.deleted_at ? '' : ``}
-
-
- ${f.deleted_at ? `` : ''}
+
+ ${f.resident ? `` : ''}
+ ${f.topic ? `
+ ${f.ai_status === 'error' ? `` : ''}
+ ${f.deleted_at ? '' : ``}
+
+
+ ${f.deleted_at ? `` : ''}
+
${escapeHtml(f.topic)}
` : ''}
+ ${text ? `${escapeHtml(text)}
` : ''}