init here

This commit is contained in:
2025-11-26 21:32:41 +03:00
commit 33c97acade
91 changed files with 9155 additions and 0 deletions

36
frontend/app.js Normal file
View File

@@ -0,0 +1,36 @@
import { routerNavigate, route } from "./router.js";
import {
loadSession,
clearSession,
setTopUser,
setLogoutVisible,
} from "./utils.js";
import "./views/auth.js";
import "./views/teacher.js";
import "./views/student.js";
route("/404", async ({ view }) => {
const card = document.createElement("div");
card.className = "card";
card.innerHTML = `<div class="card-header">Страница не найдена</div><div class="card-body">Нет такой страницы</div>`;
view.appendChild(card);
});
function initTop() {
const s = loadSession();
if (s) {
setTopUser(s.username);
setLogoutVisible(true);
}
document.getElementById("btn-logout").addEventListener("click", () => {
clearSession();
setTopUser("");
setLogoutVisible(false);
location.hash = "#/auth/login";
});
}
window.addEventListener("DOMContentLoaded", () => {
initTop();
routerNavigate();
});

53
frontend/index.html Normal file
View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Электронная школа — SRAB</title>
<link rel="icon" type="image/jpeg" href="srab.jpg" />
<link rel="apple-touch-icon" href="srab.jpg" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="app">
<header class="topbar">
<div class="brand">
<span class="logo">
<img src="srab.jpg" width="50" />
</span>
<span>Электронная Школа</span>
</div>
<div class="top-actions">
<div
class="avg"
id="top-avg"
title="Средний балл (по выбранному)"
></div>
<div class="user" id="top-user"></div>
<button id="btn-logout" class="linklike" hidden>Выйти</button>
</div>
</header>
<div class="layout">
<nav class="sidebar">
<div class="menu-title">Меню</div>
<ul id="menu">
<li><a href="#/dashboard">Дневник</a></li>
<li><a href="#/schedule">Расписание</a></li>
<li><a href="#/grades">Оценки</a></li>
<li><a href="#/school">Школа</a></li>
<li><a href="#/homework">Домашнее задание</a></li>
<li><a href="#/portfolio">Портфолио</a></li>
<li class="sep"></li>
<li><a href="#/auth/login">Войти</a></li>
<li><a href="#/auth/register">Регистрация учителя</a></li>
</ul>
</nav>
<main id="view" class="content"></main>
</div>
</div>
<script type="module" src="app.js"></script>
</body>
</html>

140
frontend/kek.js Normal file
View File

@@ -0,0 +1,140 @@
const API_BASE = (localStorage.getItem("srab.apiBase") || "").replace(
/\/$/,
""
);
function buildAuthHeader(session) {
if (!session || !session.username || !session.password) return {};
return { Authorization: `Basic ${session.username} ${session.password}` };
}
async function apiFetch(
path,
{ method = "GET", body, session, headers = {} } = {}
) {
const url = `${API_BASE}${path}`;
const h = {
"Content-Type": "application/json",
...buildAuthHeader(session),
...headers,
};
const opts = { method, headers: h };
if (body !== undefined)
opts.body = typeof body === "string" ? body : JSON.stringify(body);
const res = await fetch(url, opts);
let data = null;
const ct = res.headers.get("content-type") || "";
data = await res.text();
try {
data = JSON.parse(data);
} catch (e) {}
if (!res.ok) {
const err = new Error(
(data && (data.message || data.error)) || `HTTP ${res.status}`
);
err.status = res.status;
err.data = data;
throw err;
}
return { status: res.status, data };
}
export const Api = {
base: API_BASE,
setBase(url) {
localStorage.setItem("srab.apiBase", url);
},
async login({ username, password }) {
const session = { username, password };
const { data } = await apiFetch("/api/users/login", {
method: "POST",
session,
body: { "имя пользователя": username, пароль: password },
});
return { session, message: data };
},
async registerTeacher(payload) {
return apiFetch("/api/users", { method: "POST", body: payload });
},
async changePassword(session, newPassword) {
return apiFetch("/api/users/password", {
method: "PUT",
session,
body: { "новый пароль": newPassword },
});
},
async listTeachers(session, page) {
return apiFetch(
`/api/teachers${page ? `?страница=${encodeURIComponent(page)}` : ""}`,
{ session }
);
},
async createStudent(session, payload) {
return apiFetch("/api/students", {
method: "POST",
session,
body: payload,
});
},
async listStudents(session, teacherId) {
const q = teacherId ? `?учитель=${encodeURIComponent(teacherId)}` : "";
return apiFetch(`/api/students${q}`, { session });
},
async getStudent(session, id) {
return apiFetch(`/api/students/${id}`, { session });
},
async deleteStudent(session, id) {
return apiFetch(`/api/students/${id}`, { method: "DELETE", session });
},
async listClasses(session, teacherId) {
const q = teacherId ? `?учитель=${encodeURIComponent(teacherId)}` : "";
return apiFetch(`/api/classes${q}`, { session });
},
async createClass(session, payload) {
return apiFetch("/api/classes", { method: "POST", session, body: payload });
},
async deleteClass(session, classId) {
return apiFetch(`/api/classes/${classId}`, { method: "DELETE", session });
},
async addStudentToClass(session, classId, studentId) {
return apiFetch(`/api/classes/${classId}/students/${studentId}`, {
method: "POST",
session,
});
},
async removeStudentFromClass(session, classId, studentId) {
return apiFetch(`/api/classes/${classId}/students/${studentId}`, {
method: "DELETE",
session,
});
},
async createLesson(session, classId, payload) {
return apiFetch(`/api/classes/${classId}/lessons`, {
method: "POST",
session,
body: payload,
});
},
async listLessons(session, classId, date) {
const q = date ? `?date=${encodeURIComponent(date)}` : "";
return apiFetch(`/api/classes/${classId}/lessons${q}`, { session });
},
async listLessonsByDate(session, classId, date) {
return apiFetch(`/api/classes/${classId}/lessons/date/${date}`, {
session,
});
},
async deleteLessonById(session, classId, lessonId) {
return apiFetch(`/api/classes/${classId}/lessons/${lessonId}`, {
method: "DELETE",
session,
});
},
};

23
frontend/router.js Normal file
View File

@@ -0,0 +1,23 @@
const routes = {};
export function route(path, handler) {
routes[path] = handler;
}
function parseHash() {
const h = location.hash.replace(/^#/, "");
const parts = h.split("?");
const path = parts[0] || "/dashboard";
const search = new URLSearchParams(parts[1] || "");
return { path, search };
}
export async function routerNavigate() {
const { path, search } = parseHash();
const view = document.getElementById("view");
const handler = routes[path] || routes["/404"];
view.innerHTML = "";
await handler({ view, search });
window.scrollTo(0, 0);
}
window.addEventListener("hashchange", routerNavigate);

BIN
frontend/srab.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

236
frontend/styles.css Normal file
View File

@@ -0,0 +1,236 @@
:root {
--bg: #f6f1df;
--accent: #d6b87a;
--accent-strong: #c39a3a;
--text: #3a3a3a;
--muted: #8a8a8a;
--panel: #fffaf0;
--border: #e0d7c4;
--ok: #3aa76d;
--warn: #c97c2a;
--danger: #c43e3e;
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell,
"Noto Sans", sans-serif;
background: var(--bg);
color: var(--text);
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
background: var(--accent);
color: #fff;
border-bottom: 2px solid var(--accent-strong);
}
.topbar .brand {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
}
.topbar .logo {
filter: grayscale(20%);
}
.topbar .top-actions {
display: flex;
align-items: center;
gap: 16px;
}
.topbar .linklike {
background: none;
border: none;
color: #fff;
cursor: pointer;
text-decoration: underline;
}
.layout {
display: grid;
grid-template-columns: 240px 1fr;
min-height: calc(100vh - 44px);
}
.sidebar {
background: var(--panel);
border-right: 1px solid var(--border);
padding: 10px 0;
}
.sidebar .menu-title {
padding: 10px 16px;
color: var(--muted);
text-transform: uppercase;
font-size: 12px;
}
.sidebar ul {
list-style: none;
margin: 0;
padding: 0;
}
.sidebar li {
border-top: 1px solid var(--border);
}
.sidebar li.sep {
height: 10px;
border: none;
}
.sidebar a {
display: block;
padding: 12px 16px;
color: var(--text);
text-decoration: none;
}
.sidebar a:hover {
background: #fff;
}
.content {
padding: 16px;
}
.card {
background: #fff;
border: 1px solid var(--border);
border-radius: 6px;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03);
}
.card .card-header {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
background: #fffdf7;
font-weight: 600;
}
.card .card-body {
padding: 16px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.input {
display: flex;
flex-direction: column;
gap: 6px;
}
.input label {
color: var(--muted);
font-size: 12px;
}
.input input,
.input select,
.input textarea {
padding: 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: #fff;
}
.actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.btn {
background: var(--accent-strong);
color: #fff;
border: none;
border-radius: 6px;
padding: 10px 14px;
cursor: pointer;
}
.btn.secondary {
background: #8b8b8b;
}
.btn.danger {
background: var(--danger);
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 10px;
border-bottom: 1px solid var(--border);
text-align: left;
}
.table th {
background: #fffdf7;
color: var(--muted);
font-weight: 600;
font-size: 13px;
}
.badge {
display: inline-block;
padding: 2px 6px;
border-radius: 10px;
font-size: 12px;
background: #eee;
}
.badge.ok {
background: #e6f6ed;
color: var(--ok);
}
.badge.warn {
background: #fff1e3;
color: var(--warn);
}
.notice {
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 6px;
background: #fffbe6;
}
.error {
padding: 10px 14px;
border: 1px solid var(--danger);
border-radius: 6px;
background: #fff0f0;
color: var(--danger);
}
.tabs {
display: flex;
gap: 8px;
border-bottom: 1px solid var(--border);
margin-bottom: 12px;
}
.tabs a {
padding: 8px 12px;
text-decoration: none;
color: var(--text);
border: 1px solid transparent;
border-radius: 6px 6px 0 0;
}
.tabs a.active {
background: #fff;
border-color: var(--border);
border-bottom-color: #fff;
}
.small {
font-size: 12px;
color: var(--muted);
}
.grid-two {
display: grid;
grid-template-columns: 360px 1fr;
gap: 16px;
}
.hidden {
display: none !important;
}
.center {
text-align: center;
}

112
frontend/utils.js Normal file
View File

@@ -0,0 +1,112 @@
export const $ = (sel, root = document) => root.querySelector(sel);
export const $$ = (sel, root = document) =>
Array.from(root.querySelectorAll(sel));
export function el(tag, attrs = {}, children = []) {
const node = document.createElement(tag);
Object.entries(attrs).forEach(([k, v]) => {
if (k === "class") node.className = v;
else if (k.startsWith("on") && typeof v === "function")
node.addEventListener(k.slice(2), v);
else if (v !== undefined && v !== null) node.setAttribute(k, v);
});
for (const ch of Array.isArray(children) ? children : [children]) {
if (typeof ch === "string") node.appendChild(document.createTextNode(ch));
else if (ch) node.appendChild(ch);
}
return node;
}
export function formToJSON(form) {
const data = {};
const coerceNumberIfNeeded = (key, value) => {
const field = form.elements.namedItem(key);
let isNumber = false;
let integerOnly = false;
const inspect = (el) => {
if (!el) return;
if (el.getAttribute && el.getAttribute("data-type") === "number") {
isNumber = true;
}
if (el.getAttribute && el.getAttribute("data-integer") === "true") {
isNumber = true;
integerOnly = true;
}
if (el.tagName === "INPUT") {
const type = (el.type || "").toLowerCase();
if (type === "number") {
isNumber = true;
const step = el.step;
if (
step &&
(step === "1" || (!step.includes(".") && step !== "any"))
) {
integerOnly = true;
}
}
}
};
if (
field &&
typeof RadioNodeList !== "undefined" &&
field instanceof RadioNodeList
) {
inspect(field[0]);
} else {
inspect(field);
}
if (isNumber) {
if (value === "" || value === null) return null;
const n = Number(value);
if (Number.isNaN(n)) return null;
return integerOnly ? Math.trunc(n) : n;
}
return value;
};
new FormData(form).forEach((v, k) => {
const val = coerceNumberIfNeeded(k, v);
if (data[k] !== undefined) {
if (!Array.isArray(data[k])) data[k] = [data[k]];
data[k].push(val);
} else data[k] = val;
});
return data;
}
export function saveSession(session) {
localStorage.setItem("srab.session", JSON.stringify(session));
}
export function loadSession() {
try {
return JSON.parse(localStorage.getItem("srab.session")) || null;
} catch {
return null;
}
}
export function clearSession() {
localStorage.removeItem("srab.session");
}
export function setTopUser(text) {
$("#top-user").textContent = text || "";
}
export function setLogoutVisible(vis) {
$("#btn-logout").hidden = !vis;
}
export function setAvg(text) {
const n = $("#top-avg");
n.textContent = text || "";
n.className = "avg badge ok";
}
export function fmtDateInput(d = new Date()) {
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${yyyy}-${mm}-${dd}`;
}

86
frontend/views/auth.js Normal file
View File

@@ -0,0 +1,86 @@
import { route } from "../router.js";
import { Api } from "../kek.js";
import {
saveSession,
setTopUser,
setLogoutVisible,
formToJSON,
} from "../utils.js";
function loginView() {
const card = document.createElement("div");
card.className = "card";
card.innerHTML = `
<div class="card-header">Вход</div>
<div class="card-body">
<form id="login-form" class="form-grid" autocomplete="on">
<div class="input"><label>Имя пользователя</label><input name="username" required /></div>
<div class="input"><label>Пароль</label><input name="password" type="password" required /></div>
<div class="actions">
<button class="btn" type="submit">Войти</button>
<a class="btn secondary" href="#/auth/register">Регистрация учителя</a>
</div>
<div id="login-error" class="error hidden"></div>
</form>
</div>`;
const form = card.querySelector("#login-form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const { username, password } = Object.fromEntries(new FormData(form));
try {
const { session } = await Api.login({ username, password });
saveSession(session);
setTopUser(username);
setLogoutVisible(true);
location.hash = "#/dashboard";
} catch (err) {
const box = card.querySelector("#login-error");
box.classList.remove("hidden");
box.textContent = err.message || "Не удалось войти";
}
});
return card;
}
function registerView() {
const card = document.createElement("div");
card.className = "card";
card.innerHTML = `
<div class="card-header">Регистрация учителя</div>
<div class="card-body">
<form id="reg-form" class="form-grid">
<div class="input"><label>Имя</label><input name="имя" required></div>
<div class="input"><label>Фамилия</label><input name="фамилия" required></div>
<div class="input"><label>Отчество</label><input name="отчество" required></div>
<div class="input"><label>Образование</label><input name="образование" required></div>
<div class="input"><label>Пароль</label><input type="password" name="пароль" required></div>
<div class="input"><label>Повтор пароля</label><input type="password" name="повтор пароля" required></div>
<div class="actions"><button class="btn" type="submit">Создать</button></div>
<div id="reg-msg" class="notice hidden"></div>
<div id="reg-err" class="error hidden"></div>
</form>
</div>`;
card.querySelector("#reg-form").addEventListener("submit", async (e) => {
e.preventDefault();
const payload = formToJSON(e.target);
try {
await Api.registerTeacher(payload);
const msg = card.querySelector("#reg-msg");
msg.classList.remove("hidden");
msg.textContent = "Учитель создан. Теперь можно войти.";
card.querySelector("#reg-err").classList.add("hidden");
} catch (err) {
const errBox = card.querySelector("#reg-err");
errBox.classList.remove("hidden");
errBox.textContent = err.message || "Ошибка регистрации";
}
});
return card;
}
route("/auth/login", async ({ view }) => {
view.appendChild(loginView());
});
route("/auth/register", async ({ view }) => {
view.appendChild(registerView());
});

98
frontend/views/student.js Normal file
View File

@@ -0,0 +1,98 @@
import { route } from "../router.js";
import { Api } from "../kek.js";
import { el, $, loadSession } from "../utils.js";
function needSession() {
const s = loadSession();
if (!s) location.hash = "#/auth/login";
return s;
}
function studentDashboard(session) {
const card = el("div", { class: "card" }, [
el("div", { class: "card-header" }, "Дневник ученика"),
el("div", { class: "card-body" }, [
(() => {
const box = el("div", { class: "form-grid" });
box.innerHTML = `
<div class="input"><label>ID класса</label><input id="st-class-id" type="number" min="1"></div>
<div class="input"><label>Дата</label><input id="st-date" type="date"></div>
<div class="actions" style="align-self:end"><button id="st-load" class="btn" type="button">Показать</button></div>`;
return box;
})(),
el("table", { class: "table", id: "st-lessons" }, [
el(
"thead",
{},
el("tr", {}, [
el("th", {}, "Дата"),
el("th", {}, "Предмет/тема"),
el("th", {}, "Домашнее задание"),
])
),
el("tbody"),
]),
]),
]);
async function refresh() {
const classId = Number($("#st-class-id", card)?.value || 0);
const date = $("#st-date", card)?.value || undefined;
if (!classId) return;
const { data } = await Api.listLessons(session, classId, date);
const tbody = card.querySelector("tbody");
tbody.innerHTML = "";
for (const l of data?.["уроки"] || []) {
const tr = el("tr");
tr.appendChild(el("td", {}, l["дата"] || ""));
tr.appendChild(el("td", {}, l["название"] || ""));
tr.appendChild(el("td", {}, l["домашнее задание"] || ""));
tbody.appendChild(tr);
}
}
card.querySelector("#st-load")?.addEventListener("click", refresh);
return card;
}
function changePasswordView(session) {
const card = el("div", { class: "card" }, [
el("div", { class: "card-header" }, "Смена пароля"),
el("div", { class: "card-body" }, [
(() => {
const form = el("form", { class: "form-grid" });
form.innerHTML = `
<div class="input"><label>Новый пароль</label><input name="password" type="password" required></div>
<div class="actions"><button class="btn">Изменить</button></div>
<div id="pw-msg" class="notice hidden"></div>
<div id="pw-err" class="error hidden"></div>`;
form.addEventListener("submit", async (e) => {
e.preventDefault();
const newPassword = new FormData(form).get("password");
try {
await Api.changePassword(session, newPassword);
const msg = form.querySelector("#pw-msg");
msg.classList.remove("hidden");
msg.textContent = "Пароль изменён";
form.querySelector("#pw-err").classList.add("hidden");
} catch (err) {
const box = form.querySelector("#pw-err");
box.classList.remove("hidden");
box.textContent = err.message || "Не удалось изменить пароль";
}
});
return form;
})(),
]),
]);
return card;
}
route("/student/dashboard", async ({ view }) => {
const s = needSession();
view.appendChild(studentDashboard(s));
});
route("/student/password", async ({ view }) => {
const s = needSession();
view.appendChild(changePasswordView(s));
});

406
frontend/views/teacher.js Normal file
View File

@@ -0,0 +1,406 @@
import { route } from "../router.js";
import { Api } from "../kek.js";
import { el, formToJSON, $, fmtDateInput, loadSession } from "../utils.js";
function needSession() {
const s = loadSession();
if (!s) location.hash = "#/auth/login";
return s;
}
function sectionTabs(active) {
const tabs = document.createElement("div");
tabs.className = "tabs";
const items = [
["#/dashboard", "Сводная"],
["#/teacher/classes", "Классы"],
["#/teacher/students", "Ученики"],
["#/teacher/lessons", "Уроки"],
];
for (const [href, title] of items) {
const a = document.createElement("a");
a.href = href;
a.textContent = title;
if (href.endsWith(active)) a.classList.add("active");
tabs.appendChild(a);
}
return tabs;
}
function classesView(session) {
const wrap = el("div", { class: "grid-two" });
const formCard = el("div", { class: "card" }, [
el("div", { class: "card-header" }, "Создать класс"),
el("div", { class: "card-body" }, [
(() => {
const form = el("form", { class: "form-grid" });
form.innerHTML = `
<div class="input"><label>Номер</label><input name="номер" type="number" required min="1" max="11"></div>
<div class="input"><label>Буква</label><input name="буква" maxlength="2" required></div>
<div class="actions"><button class="btn">Создать</button></div>
<div class="small">Ограничение: однобуквенное обозначение параллели.</div>
<div id="cls-err" class="error hidden"></div>`;
form.addEventListener("submit", async (e) => {
e.preventDefault();
const payload = formToJSON(form);
try {
await Api.createClass(session, payload);
await refresh();
form.reset();
} catch (err) {
const box = form.querySelector("#cls-err");
box.classList.remove("hidden");
box.textContent = err.message || "Ошибка создания класса";
}
});
return form;
})(),
]),
]);
const listCard = el("div", { class: "card" }, [
el("div", { class: "card-header" }, "Ваши классы"),
el("div", { class: "card-body" }, [
(() => {
const box = el("div", {
class: "form-grid",
style: "margin-bottom:8px",
});
box.innerHTML = `
<div class="input"><label>ID ученика (для добавления/удаления)</label><input id="st-to-add" type="number" min="1"></div>
<div class="small">Выберите класс в таблице и используйте кнопки «Добавить ученика»/«Убрать ученика»</div>`;
return box;
})(),
el("table", { class: "table", id: "class-table" }, [
el(
"thead",
{},
el("tr", {}, [
el("th", {}, "ID"),
el("th", {}, "Класс"),
el("th", {}, "Создатель"),
el("th", {}, ""),
])
),
el("tbody"),
]),
]),
]);
wrap.appendChild(formCard);
wrap.appendChild(listCard);
async function refresh() {
const { data } = await Api.listClasses(session);
const tbody = listCard.querySelector("tbody");
tbody.innerHTML = "";
for (const c of data?.["классы"] || []) {
const tr = el("tr");
tr.appendChild(el("td", {}, String(c["идентификатор"])));
tr.appendChild(
el("td", {}, `${c["номер"] || ""}${c["буква"] ? "-" + c["буква"] : ""}`)
);
tr.appendChild(el("td", {}, String(c["создатель"] ?? "")));
const actions = el("td");
const btnAdd = el("button", { class: "btn" }, "Добавить ученика");
btnAdd.addEventListener("click", async () => {
const sid = Number($("#st-to-add", listCard)?.value || 0);
if (!sid) return alert("Укажите ID ученика");
try {
await Api.addStudentToClass(session, c["идентификатор"], sid);
await refresh();
} catch (e) {
alert(e.message || "Не удалось добавить ученика");
}
});
const btnRm = el("button", { class: "btn secondary" }, "Убрать ученика");
btnRm.addEventListener("click", async () => {
const sid = Number($("#st-to-add", listCard)?.value || 0);
if (!sid) return alert("Укажите ID ученика");
try {
await Api.removeStudentFromClass(session, c["идентификатор"], sid);
await refresh();
} catch (e) {
alert(e.message || "Не удалось удалить ученика");
}
});
const btnDel = el("button", { class: "btn danger" }, "Удалить класс");
btnDel.addEventListener("click", async () => {
if (!confirm("Удалить класс?")) return;
await Api.deleteClass(session, c["идентификатор"]);
await refresh();
});
actions.appendChild(btnAdd);
actions.appendChild(btnRm);
actions.appendChild(btnDel);
tr.appendChild(actions);
tbody.appendChild(tr);
}
}
refresh().catch(console.error);
return wrap;
}
function studentsView(session) {
const wrap = el("div", { class: "grid-two" });
const formCard = el("div", { class: "card" }, [
el("div", { class: "card-header" }, "Создать ученика"),
el("div", { class: "card-body" }, [
(() => {
const form = el("form", { class: "form-grid" });
form.innerHTML = `
<div class="input"><label>Имя</label><input name="имя" required></div>
<div class="input"><label>Фамилия</label><input name="фамилия" required></div>
<div class="input"><label>Отчество</label><input name="отчество" required></div>
<div class="input"><label>СНИЛС</label><input name="снилс" required></div>
<div class="input"><label>Паспорт</label><input name="паспорт" required></div>
<div class="input"><label>Пароль</label><input type="password" name="пароль" required></div>
<div class="input"><label>Повтор пароля</label><input type="password" name="повтор пароля" required></div>
<div class="actions"><button class="btn">Создать</button></div>
<div id="st-err" class="error hidden"></div>`;
form.addEventListener("submit", async (e) => {
e.preventDefault();
const payload = formToJSON(form);
try {
await Api.createStudent(session, payload);
await refresh();
form.reset();
} catch (err) {
const box = form.querySelector("#st-err");
box.classList.remove("hidden");
box.textContent = err.message || "Ошибка создания ученика";
}
});
return form;
})(),
]),
]);
const listCard = el("div", { class: "card" }, [
el("div", { class: "card-header" }, "Ученики"),
el("div", { class: "card-body" }, [
el("table", { class: "table", id: "st-table" }, [
el(
"thead",
{},
el("tr", {}, [
el("th", {}, "ID"),
el("th", {}, "ФИО"),
el("th", {}, "Имя пользователя"),
el("th", {}, ""),
])
),
el("tbody"),
]),
]),
]);
wrap.appendChild(formCard);
wrap.appendChild(listCard);
async function refresh() {
const { data } = await Api.listStudents(session);
const tbody = listCard.querySelector("tbody");
tbody.innerHTML = "";
for (const s of data?.["ученики"] || []) {
const tr = el("tr");
tr.appendChild(el("td", {}, String(s["идентификатор"])));
tr.appendChild(
el(
"td",
{},
`${s["фамилия"] || ""} ${s["имя"] || ""} ${
s["отчество"] || ""
}`.trim()
)
);
tr.appendChild(el("td", {}, s["имя пользователя"] || ""));
const actions = el("td");
const btnDel = el("button", { class: "btn danger" }, "Удалить");
btnDel.addEventListener("click", async () => {
if (!confirm("Удалить ученика?")) return;
await Api.deleteStudent(session, s["идентификатор"]);
await refresh();
});
actions.appendChild(btnDel);
tr.appendChild(actions);
tbody.appendChild(tr);
}
}
refresh().catch(console.error);
return wrap;
}
function lessonsView(session) {
const wrap = el("div", { class: "grid-two" });
const formCard = el("div", { class: "card" }, [
el("div", { class: "card-header" }, "Создать урок"),
el("div", { class: "card-body" }, [
(() => {
const form = el("form", { class: "form-grid" });
form.innerHTML = `
<div class="input"><label>ID класса</label><input name="classId" required type="number" min="1"></div>
<div class="input"><label>Дата</label><input name="дата" type="date" value="${fmtDateInput()}" required></div>
<div class="input"><label>Тема</label><input name="тема" required></div>
<div class="input" style="grid-column: 1/-1"><label>Домашнее задание</label><textarea name="домашка" rows="3"></textarea></div>
<div class="actions"><button class="btn">Создать</button></div>
<div id="lsn-err" class="error hidden"></div>`;
form.addEventListener("submit", async (e) => {
e.preventDefault();
const f = new FormData(form);
const classId = Number(f.get("classId"));
const payload = {
дата: f.get("дата"),
тема: f.get("тема"),
домашка: f.get("домашка"),
};
try {
await Api.createLesson(session, classId, payload);
await refresh();
} catch (err) {
const box = form.querySelector("#lsn-err");
box.classList.remove("hidden");
box.textContent = err.message || "Ошибка создания урока";
}
});
return form;
})(),
]),
]);
const listCard = el("div", { class: "card" }, [
el("div", { class: "card-header" }, "Уроки класса"),
el("div", { class: "card-body" }, [
(() => {
const filter = el("div", {
class: "form-grid",
style: "margin-bottom:8px",
});
filter.innerHTML = `
<div class="input"><label>ID класса</label><input id="flt-class" type="number" min="1"></div>
<div class="input"><label>Дата</label><input id="flt-date" type="date"></div>
<div class="actions" style="align-self:end"><button id="btn-load" class="btn" type="button">Загрузить</button></div>`;
return filter;
})(),
el("table", { class: "table", id: "lsn-table" }, [
el(
"thead",
{},
el("tr", {}, [
el("th", {}, "ID"),
el("th", {}, "Класс"),
el("th", {}, "Дата"),
el("th", {}, "Тема"),
el("th", {}, "Д/З"),
el("th", {}, ""),
])
),
el("tbody"),
]),
]),
]);
wrap.appendChild(formCard);
wrap.appendChild(listCard);
async function refresh() {
const classId = Number($("#flt-class", listCard)?.value || 0);
const date = $("#flt-date", listCard)?.value || undefined;
if (!classId) return;
const { data } = await Api.listLessons(session, classId, date);
const tbody = listCard.querySelector("tbody");
tbody.innerHTML = "";
for (const l of data?.["уроки"] || []) {
const tr = el("tr");
tr.appendChild(el("td", {}, String(l["идентификатор"])));
tr.appendChild(el("td", {}, String(l["идентификатор класса"])));
tr.appendChild(el("td", {}, l["дата"] || ""));
tr.appendChild(el("td", {}, l["название"] || ""));
tr.appendChild(el("td", {}, l["домашнее задание"] || ""));
const actions = el("td");
const btnDel = el("button", { class: "btn danger" }, "Удалить");
btnDel.addEventListener("click", async () => {
if (!confirm("Удалить урок?")) return;
await Api.deleteLessonById(
session,
l["идентификатор класса"],
l["идентификатор"]
);
await refresh();
});
actions.appendChild(btnDel);
tr.appendChild(actions);
tbody.appendChild(tr);
}
}
$("#btn-load", listCard)?.addEventListener("click", refresh);
return wrap;
}
function dashboardView() {
const card = el("div", { class: "card" }, [
el("div", { class: "card-header" }, "Сводная"),
el("div", { class: "card-body" }, [
el(
"p",
{},
"Добро пожаловать! Используйте боковое меню, чтобы работать с данными."
),
el(
"p",
{ class: "small" },
`Стоят пассажиры в аэропорту, посадочный досмотр проходят. Дошла очередь до мужика с чемоданом.
— Пожалуйста, приготовьте чемодан к осмотру.
Не могу.
— Почему?
А у меня там бипки!
А что это?
Ну, пропатчите сервис на тривиле — покажу!
Служба безопасности шутку не поняла, вызвала наряд полиции:
— Гражданин, открывайте чемодан.
Но я не могу!
— Почему не можете?
— Потому что у меня там бипки!
— «Бипки»? Что это?
— Так реализуйте гарбадж коллектор мне, и я покажу!
Отобрали чемодан, а открыть никто не может. Повезли мужика в СИЗО. В камере сидельцы расспрашивают:
— Братуха, за что тебя?
— Да чемодан отказался открывать.
А что там?
— Да бипки там.
— Какие еще «бипки»?
Ну документацию обновите мне, тогда и расскажу.
И вот угодил мужик в лазарет, с побоями да синяками по всему телу, весь перебинтован, еле дышит. Следователи вызвали группу специалистов для вскрытия чемодана. Час, два, три пыхтели. Кое-как разворотили, смотрят — а там бипки.`
),
]),
]);
return card;
}
route("/dashboard", async ({ view }) => {
const s = needSession();
view.appendChild(sectionTabs("dashboard"));
view.appendChild(dashboardView(s));
});
route("/teacher/classes", async ({ view }) => {
const s = needSession();
view.appendChild(sectionTabs("classes"));
view.appendChild(classesView(s));
});
route("/teacher/students", async ({ view }) => {
const s = needSession();
view.appendChild(sectionTabs("students"));
view.appendChild(studentsView(s));
});
route("/teacher/lessons", async ({ view }) => {
const s = needSession();
view.appendChild(sectionTabs("lessons"));
view.appendChild(lessonsView(s));
});