batch add students: add optional group selection in batch modal
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Группы — Админ-панель</title>
|
||||
<link rel="stylesheet" href="admin.css">
|
||||
</head>
|
||||
<body data-page="groups">
|
||||
<div class="layout">
|
||||
<div class="sidebar" id="sidebar"></div>
|
||||
<div class="main">
|
||||
<h2>Группы</h2>
|
||||
<div class="add-row">
|
||||
<input type="text" id="newGroup" placeholder="Название группы">
|
||||
<button onclick="addGroup()">Добавить</button>
|
||||
</div>
|
||||
<div class="list" id="groupList"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toast" id="toast"></div>
|
||||
<script src="admin.js"></script>
|
||||
<script>
|
||||
// --- Groups ---
|
||||
const DAYS = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'];
|
||||
|
||||
async function loadGroups() {
|
||||
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
|
||||
const groups = await res.json();
|
||||
const list = document.getElementById('groupList');
|
||||
if (!groups.length) { list.innerHTML = '<div style="color:var(--muted);text-align:center;padding:32px">Нет групп</div>'; return; }
|
||||
list.innerHTML = `<div class="group-grid">` + groups.map(g => {
|
||||
const schedSet = g.day_of_week != null && g.time_start && g.time_end;
|
||||
return `
|
||||
<div class="group-card">
|
||||
<div class="group-head">
|
||||
<input class="name-input" id="name-${g.id}" value="${esc(g.name)}">
|
||||
<button class="del" onclick="delGroup(${g.id})" title="Удалить">×</button>
|
||||
</div>
|
||||
<div class="group-schedule">
|
||||
<div><label>День</label>
|
||||
<select id="dow-${g.id}">
|
||||
<option value="">—</option>
|
||||
${DAYS.map((d, i) => `<option value="${i}" ${g.day_of_week === i ? 'selected' : ''}>${d}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div><label>С</label>
|
||||
<input type="time" id="ts-${g.id}" value="${g.time_start ? g.time_start.slice(0,5) : ''}">
|
||||
</div>
|
||||
<div><label>По</label>
|
||||
<input type="time" id="te-${g.id}" value="${g.time_end ? g.time_end.slice(0,5) : ''}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="group-foot">
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="save-sched" onclick="saveGroup(${g.id})">Сохранить</button>
|
||||
<button class="btn-link" onclick="exportGroup(${g.id}, '${esc(g.name).replace(/'/g, ''')}')" title="Экспортировать записи группы">CSV</button>
|
||||
</div>
|
||||
<div class="sched-status">${schedSet ? `${DAYS[g.day_of_week]} ${g.time_start.slice(0,5)}–${g.time_end.slice(0,5)}` : 'Не настроено'}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('') + `</div>`;
|
||||
}
|
||||
|
||||
async function saveGroup(id) {
|
||||
const name = document.getElementById(`name-${id}`).value.trim();
|
||||
if (!name) return;
|
||||
const dow = document.getElementById(`dow-${id}`).value;
|
||||
const ts = document.getElementById(`ts-${id}`).value;
|
||||
const te = document.getElementById(`te-${id}`).value;
|
||||
const res = await fetch(`${API}/api/groups/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: hdrJson(),
|
||||
body: JSON.stringify({ name, day_of_week: dow !== '' ? parseInt(dow) : null, time_start: ts || null, time_end: te || null })
|
||||
});
|
||||
if (res.ok) {
|
||||
loadGroups();
|
||||
const dayLabel = dow !== '' ? DAYS[dow] : 'без расписания';
|
||||
const timeLabel = ts && te ? `, ${ts}–${te}` : '';
|
||||
showToast(`Сохранено: «${name}» — ${dayLabel}${timeLabel}`);
|
||||
} else { const e = await res.json(); alert(e.error); }
|
||||
}
|
||||
|
||||
async function addGroup() {
|
||||
const input = document.getElementById('newGroup');
|
||||
const name = input.value.trim();
|
||||
if (!name) return;
|
||||
const res = await fetch(`${API}/api/groups`, { method: 'POST', headers: hdrJson(), body: JSON.stringify({ name }) });
|
||||
if (res.ok) { input.value = ''; loadGroups(); }
|
||||
else { const e = await res.json(); alert(e.error); }
|
||||
}
|
||||
|
||||
async function delGroup(id) {
|
||||
if (!confirm('Удалить группу?')) return;
|
||||
await fetch(`${API}/api/groups/${id}`, { method: 'DELETE', headers: hdr() });
|
||||
loadGroups();
|
||||
}
|
||||
|
||||
async function exportGroup(id, name) {
|
||||
const res = await fetch(`${API}/api/entries?group_id=${id}`, { headers: hdr() });
|
||||
const { entries } = await res.json();
|
||||
if (!entries.length) { alert('Нет данных для экспорта'); return; }
|
||||
const rows = [['Фамилия Имя', 'Группа', 'Описание', 'Дата', 'Ссылка на страницу']];
|
||||
entries.forEach(e => {
|
||||
rows.push([
|
||||
e.student_name,
|
||||
e.group_name,
|
||||
e.description.replace(/\n/g, ' '),
|
||||
new Date(e.created_at).toLocaleString('ru'),
|
||||
`${location.origin}/?nameInput=${encodeURIComponent(e.student_name)}`
|
||||
]);
|
||||
});
|
||||
const csv = rows.map(r => r.map(c => `"${String(c).replace(/"/g, '""')}"`).join(';')).join('\n');
|
||||
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `group_${name.replace(/[\\/:*?"<>|]/g, '_')}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
showToast(`Экспортировано записей: ${entries.length}`);
|
||||
}
|
||||
|
||||
document.getElementById('newGroup').addEventListener('keydown', e => { if (e.key === 'Enter') addGroup(); });
|
||||
|
||||
(async () => {
|
||||
if (await checkAuth()) {
|
||||
buildSidebar(document.body.dataset.page);
|
||||
loadGroups();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user