master #1
@@ -33,6 +33,7 @@ CREATE TABLE IF NOT EXISTS share_links (
|
|||||||
token VARCHAR(40) UNIQUE NOT NULL,
|
token VARCHAR(40) UNIQUE NOT NULL,
|
||||||
name VARCHAR(200) NOT NULL,
|
name VARCHAR(200) NOT NULL,
|
||||||
group_id INT REFERENCES groups(id),
|
group_id INT REFERENCES groups(id),
|
||||||
|
student_name VARCHAR(150),
|
||||||
date_from DATE,
|
date_from DATE,
|
||||||
date_to DATE,
|
date_to DATE,
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ CREATE TABLE IF NOT EXISTS share_links (
|
|||||||
token VARCHAR(40) UNIQUE NOT NULL,
|
token VARCHAR(40) UNIQUE NOT NULL,
|
||||||
name VARCHAR(200) NOT NULL,
|
name VARCHAR(200) NOT NULL,
|
||||||
group_id INT REFERENCES groups(id),
|
group_id INT REFERENCES groups(id),
|
||||||
|
student_name VARCHAR(150),
|
||||||
date_from DATE,
|
date_from DATE,
|
||||||
date_to DATE,
|
date_to DATE,
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
|||||||
+25
-9
@@ -13,7 +13,7 @@
|
|||||||
<h2>Журнал</h2>
|
<h2>Журнал</h2>
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<div><label>Поиск</label><input type="text" id="searchInput" class="search-input" placeholder="Имя или описание"></div>
|
<div><label>Поиск</label><input type="text" id="searchInput" class="search-input" placeholder="Имя или описание"></div>
|
||||||
<div><label>Студент</label><select id="studentFilter"><option value="">Все</option></select></div>
|
<div><label>Студент</label><input type="text" id="studentFilter" list="studentFilterList" placeholder="Все / введите имя..."><datalist id="studentFilterList"></datalist></div>
|
||||||
<div><label>Группа</label><select id="groupFilter"><option value="">Все группы</option></select></div>
|
<div><label>Группа</label><select id="groupFilter"><option value="">Все группы</option></select></div>
|
||||||
<div><label>С</label><input type="date" id="dateFrom"></div>
|
<div><label>С</label><input type="date" id="dateFrom"></div>
|
||||||
<div><label>По</label><input type="date" id="dateTo"></div>
|
<div><label>По</label><input type="date" id="dateTo"></div>
|
||||||
@@ -67,6 +67,14 @@
|
|||||||
<label>Название ссылки</label>
|
<label>Название ссылки</label>
|
||||||
<input type="text" id="linkNameInput">
|
<input type="text" id="linkNameInput">
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Группа (если выбрана)</label>
|
||||||
|
<input type="text" id="linkGroupPreview" readonly style="background:var(--bg);color:var(--muted);cursor:not-allowed">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Ученик (необязательно)</label>
|
||||||
|
<input type="text" id="linkStudent" list="linkStudentList" placeholder="Все / введите имя..."><datalist id="linkStudentList"></datalist>
|
||||||
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="cancel" onclick="closeLinkCreate()">Отмена</button>
|
<button class="cancel" onclick="closeLinkCreate()">Отмена</button>
|
||||||
<button class="save" onclick="createLink()">Создать</button>
|
<button class="save" onclick="createLink()">Создать</button>
|
||||||
@@ -184,15 +192,10 @@ async function loadGroupOptions() {
|
|||||||
|
|
||||||
async function loadStudentOptions() {
|
async function loadStudentOptions() {
|
||||||
if (studentsLoaded) return;
|
if (studentsLoaded) return;
|
||||||
const sel = document.getElementById('studentFilter');
|
const dl = document.getElementById('studentFilterList');
|
||||||
while (sel.options.length > 1) sel.remove(1);
|
|
||||||
const res = await fetch(`${API}/api/students/names`, { headers: hdr() });
|
const res = await fetch(`${API}/api/students/names`, { headers: hdr() });
|
||||||
const names = await res.json();
|
const names = await res.json();
|
||||||
names.forEach(n => {
|
dl.innerHTML = names.map(n => `<option value="${esc(n)}">`).join('');
|
||||||
const o = document.createElement('option');
|
|
||||||
o.value = n; o.textContent = n;
|
|
||||||
sel.appendChild(o);
|
|
||||||
});
|
|
||||||
studentsLoaded = true;
|
studentsLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,6 +333,7 @@ async function exportCSV() {
|
|||||||
|
|
||||||
// --- Ссылки ---
|
// --- Ссылки ---
|
||||||
let linkGroups = [];
|
let linkGroups = [];
|
||||||
|
let linkStudentsLoaded = false;
|
||||||
let createdLinkUrl = '';
|
let createdLinkUrl = '';
|
||||||
|
|
||||||
function fmtD(d) { return d ? d.split('-').reverse().join('.') : 'весь период'; }
|
function fmtD(d) { return d ? d.split('-').reverse().join('.') : 'весь период'; }
|
||||||
@@ -343,11 +347,22 @@ async function openLinkCreate() {
|
|||||||
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
|
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
|
||||||
linkGroups = await res.json();
|
linkGroups = await res.json();
|
||||||
}
|
}
|
||||||
|
if (!linkStudentsLoaded) {
|
||||||
|
const res = await fetch(`${API}/api/students/names`, { headers: hdr() });
|
||||||
|
const names = await res.json();
|
||||||
|
const dl = document.getElementById('linkStudentList');
|
||||||
|
dl.innerHTML = names.map(n => `<option value="${esc(n)}">`).join('');
|
||||||
|
linkStudentsLoaded = true;
|
||||||
|
}
|
||||||
|
// Capture current student filter
|
||||||
|
const currentStudent = document.getElementById('studentFilter').value;
|
||||||
const g = linkGroups.find(x => String(x.id) === String(gid));
|
const g = linkGroups.find(x => String(x.id) === String(gid));
|
||||||
document.getElementById('linkPreview').innerHTML =
|
document.getElementById('linkPreview').innerHTML =
|
||||||
`Будут показаны записи: <b>${esc(g ? g.name : '')}</b><br>Период: <b>${fmtD(df)}</b> — <b>${fmtD(dt)}</b>`;
|
`Будут показаны записи: <b>${esc(g ? g.name : '')}</b><br>Период: <b>${fmtD(df)}</b> — <b>${fmtD(dt)}</b>`;
|
||||||
|
document.getElementById('linkGroupPreview').value = g ? g.name : '';
|
||||||
const dfl = df.split('-').reverse().join('.');
|
const dfl = df.split('-').reverse().join('.');
|
||||||
document.getElementById('linkNameInput').value = `${g ? g.name : ''} ${dfl}`;
|
document.getElementById('linkNameInput').value = `${g ? g.name : ''} ${dfl}`;
|
||||||
|
document.getElementById('linkStudent').value = currentStudent;
|
||||||
document.getElementById('linkCreateView').style.display = '';
|
document.getElementById('linkCreateView').style.display = '';
|
||||||
document.getElementById('linkResultView').style.display = 'none';
|
document.getElementById('linkResultView').style.display = 'none';
|
||||||
document.getElementById('linkModal').classList.add('open');
|
document.getElementById('linkModal').classList.add('open');
|
||||||
@@ -360,13 +375,14 @@ function closeLinkCreate() {
|
|||||||
async function createLink() {
|
async function createLink() {
|
||||||
const name = document.getElementById('linkNameInput').value.trim();
|
const name = document.getElementById('linkNameInput').value.trim();
|
||||||
const group_id = document.getElementById('groupFilter').value;
|
const group_id = document.getElementById('groupFilter').value;
|
||||||
|
const student_name = document.getElementById('linkStudent').value;
|
||||||
const date_from = document.getElementById('dateFrom').value;
|
const date_from = document.getElementById('dateFrom').value;
|
||||||
const date_to = document.getElementById('dateTo').value;
|
const date_to = document.getElementById('dateTo').value;
|
||||||
if (!name) { alert('Введите название'); return; }
|
if (!name) { alert('Введите название'); return; }
|
||||||
const res = await fetch(`${API}/api/links`, {
|
const res = await fetch(`${API}/api/links`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: hdrJson(),
|
headers: hdrJson(),
|
||||||
body: JSON.stringify({ name, group_id, date_from: date_from || null, date_to: date_to || null })
|
body: JSON.stringify({ name, group_id, student_name: student_name || null, date_from: date_from || null, date_to: date_to || null })
|
||||||
});
|
});
|
||||||
if (!res.ok) { const err = await res.json(); alert(err.error || 'Ошибка'); return; }
|
if (!res.ok) { const err = await res.json(); alert(err.error || 'Ошибка'); return; }
|
||||||
const link = await res.json();
|
const link = await res.json();
|
||||||
|
|||||||
+29
-5
@@ -25,7 +25,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Группа</label>
|
<label>Группа</label>
|
||||||
<select id="linkGroup"></select>
|
<select id="linkGroup"><option value="">— без группы —</option></select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Ученик (необязательно)</label>
|
||||||
|
<input type="text" id="linkStudent" list="linkStudentList" placeholder="Все / введите имя..."><datalist id="linkStudentList"></datalist>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:12px">
|
<div style="display:flex;gap:12px">
|
||||||
<div style="flex:1">
|
<div style="flex:1">
|
||||||
@@ -50,6 +54,7 @@
|
|||||||
let links = [];
|
let links = [];
|
||||||
let groups = [];
|
let groups = [];
|
||||||
let editingId = null;
|
let editingId = null;
|
||||||
|
let studentsLoaded = false;
|
||||||
|
|
||||||
function linkUrl(token) { return `${location.origin}/s/${token}`; }
|
function linkUrl(token) { return `${location.origin}/s/${token}`; }
|
||||||
|
|
||||||
@@ -63,7 +68,8 @@ async function loadLinks() {
|
|||||||
<div style="min-width:0">
|
<div style="min-width:0">
|
||||||
<div class="name">${esc(l.name)}</div>
|
<div class="name">${esc(l.name)}</div>
|
||||||
<div style="font-size:.8rem;color:var(--muted);margin-top:2px">
|
<div style="font-size:.8rem;color:var(--muted);margin-top:2px">
|
||||||
Группа: <b>${esc(l.group_name || '—')}</b>
|
${l.group_name ? `Группа: <b>${esc(l.group_name)}</b>` : ''}
|
||||||
|
${l.student_name ? `${l.group_name ? ' · ' : ''}Ученик: <b>${esc(l.student_name)}</b>` : ''}
|
||||||
${l.date_from || l.date_to ? ` · период: <b>${l.date_from ? l.date_from.split('-').reverse().join('.') : '…'}</b> — <b>${l.date_to ? l.date_to.split('-').reverse().join('.') : '…'}</b>` : ' · весь период'}
|
${l.date_from || l.date_to ? ` · период: <b>${l.date_from ? l.date_from.split('-').reverse().join('.') : '…'}</b> — <b>${l.date_to ? l.date_to.split('-').reverse().join('.') : '…'}</b>` : ' · весь период'}
|
||||||
· создана ${new Date(l.created_at).toLocaleString('ru')}
|
· создана ${new Date(l.created_at).toLocaleString('ru')}
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +95,15 @@ async function openLinkModal() {
|
|||||||
if (!groups.length) {
|
if (!groups.length) {
|
||||||
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
|
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
|
||||||
groups = await res.json();
|
groups = await res.json();
|
||||||
document.getElementById('linkGroup').innerHTML = groups.map(g => `<option value="${g.id}">${esc(g.name)}</option>`).join('');
|
document.getElementById('linkGroup').innerHTML = '<option value="">— без группы —</option>' +
|
||||||
|
groups.map(g => `<option value="${g.id}">${esc(g.name)}</option>`).join('');
|
||||||
|
}
|
||||||
|
if (!studentsLoaded) {
|
||||||
|
const res = await fetch(`${API}/api/students/names`, { headers: hdr() });
|
||||||
|
const names = await res.json();
|
||||||
|
const dl = document.getElementById('linkStudentList');
|
||||||
|
dl.innerHTML = names.map(n => `<option value="${esc(n)}">`).join('');
|
||||||
|
studentsLoaded = true;
|
||||||
}
|
}
|
||||||
document.getElementById('linkModal').classList.add('open');
|
document.getElementById('linkModal').classList.add('open');
|
||||||
}
|
}
|
||||||
@@ -105,9 +119,18 @@ async function openLinkEdit(id) {
|
|||||||
if (!groups.length) {
|
if (!groups.length) {
|
||||||
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
|
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
|
||||||
groups = await res.json();
|
groups = await res.json();
|
||||||
document.getElementById('linkGroup').innerHTML = groups.map(g => `<option value="${g.id}">${esc(g.name)}</option>`).join('');
|
document.getElementById('linkGroup').innerHTML = '<option value="">— без группы —</option>' +
|
||||||
|
groups.map(g => `<option value="${g.id}">${esc(g.name)}</option>`).join('');
|
||||||
}
|
}
|
||||||
document.getElementById('linkGroup').value = l.group_id || '';
|
document.getElementById('linkGroup').value = l.group_id || '';
|
||||||
|
if (!studentsLoaded) {
|
||||||
|
const res = await fetch(`${API}/api/students/names`, { headers: hdr() });
|
||||||
|
const names = await res.json();
|
||||||
|
const dl = document.getElementById('linkStudentList');
|
||||||
|
dl.innerHTML = names.map(n => `<option value="${esc(n)}">`).join('');
|
||||||
|
studentsLoaded = true;
|
||||||
|
}
|
||||||
|
document.getElementById('linkStudent').value = l.student_name || '';
|
||||||
document.getElementById('linkModal').classList.add('open');
|
document.getElementById('linkModal').classList.add('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,10 +142,11 @@ function closeLinkModal() {
|
|||||||
async function saveLink() {
|
async function saveLink() {
|
||||||
const name = document.getElementById('linkName').value.trim();
|
const name = document.getElementById('linkName').value.trim();
|
||||||
const group_id = document.getElementById('linkGroup').value;
|
const group_id = document.getElementById('linkGroup').value;
|
||||||
|
const student_name = document.getElementById('linkStudent').value;
|
||||||
const date_from = document.getElementById('linkDateFrom').value;
|
const date_from = document.getElementById('linkDateFrom').value;
|
||||||
const date_to = document.getElementById('linkDateTo').value;
|
const date_to = document.getElementById('linkDateTo').value;
|
||||||
if (!name) { alert('Введите название'); return; }
|
if (!name) { alert('Введите название'); return; }
|
||||||
const body = JSON.stringify({ name, group_id: group_id || null, date_from: date_from || null, date_to: date_to || null });
|
const body = JSON.stringify({ name, group_id: group_id || null, student_name: student_name || null, date_from: date_from || null, date_to: date_to || null });
|
||||||
const res = await fetch(editingId ? `${API}/api/links/${editingId}` : `${API}/api/links`, {
|
const res = await fetch(editingId ? `${API}/api/links/${editingId}` : `${API}/api/links`, {
|
||||||
method: editingId ? 'PUT' : 'POST',
|
method: editingId ? 'PUT' : 'POST',
|
||||||
headers: hdrJson(),
|
headers: hdrJson(),
|
||||||
|
|||||||
@@ -239,25 +239,25 @@ app.get('/api/links', requireAdmin, async (_, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/links', requireAdmin, async (req, res) => {
|
app.post('/api/links', requireAdmin, async (req, res) => {
|
||||||
const { name, group_id, date_from, date_to } = req.body;
|
const { name, group_id, student_name, date_from, date_to } = req.body;
|
||||||
if (!name?.trim()) return res.status(400).json({ error: 'Название обязательно' });
|
if (!name?.trim()) return res.status(400).json({ error: 'Название обязательно' });
|
||||||
const token = crypto.randomBytes(20).toString('hex');
|
const token = crypto.randomBytes(20).toString('hex');
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`INSERT INTO share_links (token, name, group_id, date_from, date_to)
|
`INSERT INTO share_links (token, name, group_id, student_name, date_from, date_to)
|
||||||
VALUES ($1, $2, $3, $4, $5) RETURNING *`,
|
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
|
||||||
[token, name.trim(), group_id || null, date_from || null, date_to || null]
|
[token, name.trim(), group_id || null, student_name || null, date_from || null, date_to || null]
|
||||||
);
|
);
|
||||||
res.status(201).json(normDates(rows[0]));
|
res.status(201).json(normDates(rows[0]));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.put('/api/links/:id', requireAdmin, async (req, res) => {
|
app.put('/api/links/:id', requireAdmin, async (req, res) => {
|
||||||
const { name, group_id, date_from, date_to } = req.body;
|
const { name, group_id, student_name, date_from, date_to } = req.body;
|
||||||
if (!name?.trim()) return res.status(400).json({ error: 'Название обязательно' });
|
if (!name?.trim()) return res.status(400).json({ error: 'Название обязательно' });
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`UPDATE share_links SET
|
`UPDATE share_links SET
|
||||||
name = $1, group_id = $2, date_from = $3, date_to = $4
|
name = $1, group_id = $2, student_name = $3, date_from = $4, date_to = $5
|
||||||
WHERE id = $5 RETURNING *`,
|
WHERE id = $6 RETURNING *`,
|
||||||
[name.trim(), group_id || null, date_from || null, date_to || null, req.params.id]
|
[name.trim(), group_id || null, student_name || null, date_from || null, date_to || null, req.params.id]
|
||||||
);
|
);
|
||||||
if (!rows.length) return res.status(404).json({ error: 'Не найдено' });
|
if (!rows.length) return res.status(404).json({ error: 'Не найдено' });
|
||||||
res.json(normDates(rows[0]));
|
res.json(normDates(rows[0]));
|
||||||
@@ -280,6 +280,7 @@ app.get('/api/share/:token', async (req, res) => {
|
|||||||
const conditions = [];
|
const conditions = [];
|
||||||
const params = [];
|
const params = [];
|
||||||
if (l.group_id) { params.push(l.group_id); conditions.push(`e.group_id = $${params.length}`); }
|
if (l.group_id) { params.push(l.group_id); conditions.push(`e.group_id = $${params.length}`); }
|
||||||
|
if (l.student_name) { params.push(l.student_name); conditions.push(`e.student_name = $${params.length}`); }
|
||||||
if (l.date_from) { params.push(l.date_from); conditions.push(`e.created_at >= $${params.length}::date`); }
|
if (l.date_from) { params.push(l.date_from); conditions.push(`e.created_at >= $${params.length}::date`); }
|
||||||
if (l.date_to) { params.push(l.date_to); conditions.push(`e.created_at < ($${params.length}::date + interval '1 day')`); }
|
if (l.date_to) { params.push(l.date_to); conditions.push(`e.created_at < ($${params.length}::date + interval '1 day')`); }
|
||||||
const where = conditions.length ? ' WHERE ' + conditions.join(' AND ') : '';
|
const where = conditions.length ? ' WHERE ' + conditions.join(' AND ') : '';
|
||||||
@@ -291,6 +292,7 @@ app.get('/api/share/:token', async (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
name: l.name,
|
name: l.name,
|
||||||
group_name: l.group_name,
|
group_name: l.group_name,
|
||||||
|
student_name: l.student_name,
|
||||||
group_id: l.group_id,
|
group_id: l.group_id,
|
||||||
date_from: l.date_from,
|
date_from: l.date_from,
|
||||||
date_to: l.date_to,
|
date_to: l.date_to,
|
||||||
|
|||||||
Reference in New Issue
Block a user