<!doctype html>

<html lang="ru">

<head>

<meta charset="utf-8">

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

<title>ВСЕ УСПЕТЬ</title>

<script src="https://telegram.org/js/telegram-web-app.js"></script>

<link rel="preconnect" href="https://fonts.googleapis.com">

<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">

<style>

:root{--bg:#0A0A0A;--fg:#F5F5F5;--red:#D71920;--mut:#BDBDBD;--line:#333;--field:#151515}

*{box-sizing:border-box}

html,body{margin:0;background:var(--bg);color:var(--fg);font-family:Inter,system-ui,sans-serif;-webkit-font-smoothing:antialiased}

body{padding:env(safe-area-inset-top,0px) 0 env(safe-area-inset-bottom,0px)}

.wrap{max-width:520px;margin:0 auto;padding:20px 18px 70px;display:flex;flex-direction:column;gap:18px}

.display,h1,h2{font-family:Unbounded,sans-serif;font-weight:900;margin:0;letter-spacing:-.5px}

h2{font-size:20px;font-weight:700;letter-spacing:0}

.top{display:flex;justify-content:space-between;align-items:center}

.logo{font-family:Unbounded,sans-serif;font-weight:900;font-size:22px;letter-spacing:-.5px}

.bar{height:4px;width:64px;background:var(--red)}

.tabs{display:flex;border:1px solid var(--fg);border-radius:999px;padding:4px}

.tabs button{flex:1;background:none;border:0;color:var(--fg);padding:10px;border-radius:999px;font:inherit;font-weight:600;cursor:pointer}

.tabs button.on{background:var(--fg);color:var(--bg)}

.card{background:var(--fg);color:var(--bg);border-radius:20px;padding:20px;display:flex;flex-direction:column;gap:12px}

.box{border:1px solid var(--line);border-radius:16px;padding:14px 16px}

.box.alert{border-color:var(--red)}

.row{display:flex;justify-content:space-between;align-items:center;gap:12px}

.col{display:flex;flex-direction:column;gap:4px}

.muted{color:var(--mut);font-size:13px}

.uc{font-size:12px;font-weight:600;letter-spacing:1px;text-transform:uppercase}

.btn{display:flex;align-items:center;justify-content:center;text-decoration:none;background:var(--red);color:var(--fg);border:0;border-radius:14px;height:52px;padding:0 18px;font:inherit;font-weight:700;font-size:16px;cursor:pointer;width:100%}

.btn.dark{background:var(--bg);color:var(--fg)}

.btn.light{background:var(--fg);color:var(--bg)}

.btn.sm{height:40px;width:auto;border-radius:12px;padding:0 14px;font-size:14px}

.btn.line{background:none;border:1px solid var(--fg);color:var(--fg)}

.tag{font-size:12px;font-weight:700;border:1px solid currentColor;border-radius:999px;padding:4px 10px;white-space:nowrap;align-self:flex-start}

.tag.red{color:var(--red)}

.done{color:#7a7a7a}

.done .title{text-decoration:line-through}

.check{width:26px;height:26px;border:2px solid currentColor;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;flex:none;background:none;cursor:pointer;color:var(--fg);padding:0}

.check.on{background:currentColor}

.check.on::after{content:"";width:7px;height:12px;border:solid var(--bg);border-width:0 3px 3px 0;transform:rotate(45deg) translate(-1px,-1px)}

.sec{display:flex;flex-direction:column;gap:10px}

.sec h2{display:flex;justify-content:space-between;align-items:baseline;gap:10px}

.bars{display:flex;gap:4px}

.bars i{flex:1;height:8px;border-radius:4px;background:var(--line)}

.bars i.on{background:var(--fg)}

input,select,textarea{width:100%;background:var(--field);border:1px solid var(--line);color:var(--fg);border-radius:12px;padding:12px;font:inherit;font-size:16px}

label{display:flex;flex-direction:column;gap:6px;font-size:13px;color:var(--mut)}

.grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}

.empty{color:var(--mut);font-size:14px;padding:6px 0}

details summary{cursor:pointer;list-style:none;font-weight:700}

details summary::-webkit-details-marker{display:none}

details[open] > summary{margin-bottom:12px}

.topic{display:flex;justify-content:space-between;align-items:center;width:100%;background:none;border:0;border-top:1px solid var(--line);color:var(--fg);padding:12px 0;font:inherit;cursor:pointer;text-align:left}

.subnav{display:flex;gap:6px;flex-wrap:wrap}

.subnav button{flex:1 1 30%;background:none;border:1px solid var(--line);color:var(--fg);border-radius:10px;padding:9px 6px;font:inherit;font-size:13px;cursor:pointer}

.subnav button.on{background:var(--fg);color:var(--bg)}

.mat{display:flex;flex-direction:column;gap:6px;font-size:14px}

.mat a{color:var(--fg);text-decoration:underline}

#toast{position:fixed;left:50%;bottom:24px;transform:translate(-50%,140px);background:var(--fg);color:var(--bg);padding:12px 18px;border-radius:12px;font-weight:600;transition:transform .25s;pointer-events:none}

#toast.show{transform:translate(-50%,0)}

</style>

</head>

<body>

<div class="wrap">

<div class="top"><span class="logo">ВСЕ УСПЕТЬ</span><span class="tag" id="who"></span></div>

<div class="bar"></div>

<div class="tabs">

<button data-role="student">Ученик</button>

<button data-role="tutor">Репетитор</button>

</div>

<div id="app"></div>

</div>

<div id="toast"></div>

<script>

const KEY = 'vse-uspet-v1';

const TL = 'https://telemost.yandex.ru/j/';

const OGE_TOPICS = ['Древняя Русь','Монгольское нашествие','Московское государство','Смутное время','Петр I','Екатерина II','Александр I','Николай I','Реформы 1860–1870-х'];

const EGE_TOPICS = ['Древний мир','Средние века','Новое время: XVI–XVII','Новое время: XVIII в.','XIX век','Первая мировая','Гражданская война и СССР','Вторая мировая','Послевоенный СССР','Современная Россия'];

const p2 = n => String(n).padStart(2, '0');

const iso = d => d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate());

const addDays = n => { const d = new Date(); d.setDate(d.getDate() + n); return iso(d); };

const todayIso = () => iso(new Date());

const uid = () => Math.random().toString(36).slice(2, 9);

const esc = s => String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));

const money = n => Number(n).toLocaleString('ru-RU') + ' ₽';

const lessonTs = l => new Date(l.date + 'T' + l.time + ':00').getTime();

const dayLabel = d => new Date(d + 'T00:00:00').toLocaleDateString('ru-RU', { weekday: 'short', day: 'numeric', month: 'long' });

const shortDate = d => new Date(d + 'T00:00:00').toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });

const SCHOOL_TOPICS = OGE_TOPICS; // заглушка: программа для «школьной» группы уточняется

function seed() {

const TLK = () => TL + Math.floor(1e10 + Math.random() * 9e10);

const L = (id, off, time, sid, topic, paid) => ({ id, date: addDays(off), time, dur: 60, studentId: sid, topic, paid, link: TLK() });

return {

role: 'student',

me: null,

students: [

{ id: 'aglaya', name: 'Аглая', exam: 'Школа', price: 800, username: 'dol9hin1fan', topics: SCHOOL_TOPICS, passed: [] },

{ id: 'anna', name: 'Анна', exam: 'Школа', price: 1000, username: '', topics: SCHOOL_TOPICS, passed: [] },

{ id: 'semen', name: 'Семен', exam: 'ОГЭ', price: 900, username: 'aily63', topics: OGE_TOPICS, passed: [] },

{ id: 'liza', name: 'Лиза', exam: 'ЕГЭ', price: 900, username: 'woman_in_track', topics: EGE_TOPICS, passed: [] }

],

lessons: [],

homework: []

};

}

const CFG = {

API_URL: 'https://szeobdwymbaegffcalgn.supabase.co/functions/v1/app',

TUTOR_USERNAME: 'kovvvliaaa'

};

const CFG_READY = !CFG.API_URL.includes('YOUR');

const TG = window.Telegram && window.Telegram.WebApp;

if (TG) { TG.ready(); TG.expand(); }

const ME_NAME = ((TG && TG.initDataUnsafe && TG.initDataUnsafe.user && TG.initDataUnsafe.user.username) || '').toLowerCase();

const ROLE = ME_NAME && ME_NAME === CFG.TUTOR_USERNAME.toLowerCase() ? 'tutor' : 'student';

let GUEST = false;

let NEW = false;

const HISTORY_FACTS = [

'Законы Хаммурапи записаны около 1754 года до н. э. на базальтовой стеле.',

'Битва при Марафоне произошла в 490 году до н. э.',

'Магна Карта подписана в 1215 году.',

'Санкт-Петербург основан Петром I в 1703 году.',

'Древние египтяне использовали папирус для письма, а не только камень.',

'Великая Китайская стена строилась на протяжении многих веков разными династиями.'

];

const dayIndex = n => Math.floor(Date.now() / 86400000) % n;

let S = seed();

let lastRemote = '';

function applyView(view, role) {

S = view;

if (role === 'tutor') { S.role = 'tutor'; }

else { S.role = 'student'; S.me = S.students[0] ? S.students[0].id : null; }

}

async function api(action, extra) {

const r = await fetch(CFG.API_URL, {

method: 'POST',

headers: { 'Content-Type': 'application/json' },

body: JSON.stringify(Object.assign({ action, initData: TG ? TG.initData : '', state: action === 'save' ? S : undefined }, extra || {}))

});

if (r.status === 400) return { error: (await r.json()).error };

if (!r.ok) throw new Error(String(r.status));

return r.json();

}

async function pull(action) {

if (!CFG_READY || !TG) return;

try {

const j = await api(action || 'get');

if (j.error) { toast(j.error === 'need_username' ? 'Сначала задайте username в Telegram' : 'Заполните ФИО'); return; }

if (j.role === 'new') { NEW = true; render(); return; }

NEW = false;

if (j.role === 'tutor' && !j.state.students.length) { save(); return; } // первый запуск: отправить начальные данные

const txt = JSON.stringify(j.state);

if (txt !== lastRemote) { lastRemote = txt; applyView(j.state, j.role); render(); }

} catch (e) {

if (e.message === '401' || e.message === '403') { GUEST = true; render(); return; }

if (action === 'save') toast('Не сохранилось, проверьте связь');

}

}

function save() { pull('save'); }

if (ROLE !== 'tutor') S.role = 'student';

const studentOf = id => S.students.find(s => s.id === id);

const sortLessons = arr => arr.slice().sort((a, b) => lessonTs(a) - lessonTs(b));

const upcoming = sid => sortLessons(S.lessons.filter(l => (!sid || l.studentId === sid) && l.date >= todayIso() && lessonTs(l) + l.dur * 60000 >= Date.now()));

function toast(text) {

const el = document.getElementById('toast');

el.textContent = text;

el.classList.add('show');

clearTimeout(toast.t);

toast.t = setTimeout(() => el.classList.remove('show'), 1800);

}

let SCREEN = 'home';

let TAKING = null;

const matText = l => (l.materials || []).map(m => m.title + ' | ' + m.url).join('\n');

const parseMat = t => String(t || '').split('\n').map(x => x.trim()).filter(Boolean).map(x => {

const [title, ...rest] = x.split('|'); const url = rest.join('|').trim();

return { title: (title || '').trim() || 'Материал', url };

}).filter(m => /^https?:\/\//.test(m.url));

function studentView() {

const me = studentOf(S.me) || S.students[0];

if (!me) return '<div class="box"><div class="empty">Профиль не найден</div></div>';

const ups = upcoming(me.id);

const next = ups[0];

const unpaid = ups.filter(l => !l.paid);

const mine = S.lessons.filter(l => l.studentId === me.id);

const nav = [['home', 'Главная'], ['calendar', 'Календарь'], ['scripts', 'Скрипты'], ['hw', 'ДЗ'], ['tests', 'Тесты'], ['profile', 'Профиль']];

const navHtml = `<div class="subnav">${nav.map(([k, t]) => `<button class="{k}">${t}</button>`).join('')}</div>`;

let body;

if (SCREEN === 'calendar') body = calendarScreen(sortLessons(mine.filter(l => l.date >= addDays(-7) && l.date <= addDays(13))));

else if (SCREEN === 'scripts') body = scriptsScreen(sortLessons(mine.filter(l => l.script)));

else if (SCREEN === 'hw') body = hwScreen(me, mine);

else if (SCREEN === 'tests') body = testsScreen();

else if (SCREEN === 'profile') body = profileScreen(me, unpaid);

else body = homeScreen(me, next, unpaid);

return navHtml + body;

}

function homeScreen(me, next, unpaid) {

const fact = HISTORY_FACTS[dayIndex(HISTORY_FACTS.length)];

const about = (S.teacherFacts || []).length ? S.teacherFacts[dayIndex(S.teacherFacts.length)] : '';

return `

<div class="col"><span class="uc" style="color:var(--mut)">Привет, ${esc(me.name)}</span><h1 style="font-size:30px">Главная</h1></div>

<button class="btn" data-screen="profile">Войти в профиль</button>

${next ? `

<section class="card">

<span class="uc" style="color:#3a3a3a">Ближайший урок</span>

<div class="display" style="font-size:36px;line-height:1">${next.time}</div>

<div style="font-weight:600">${dayLabel(next.date)} · ${esc(next.topic)}</div>

<a class="btn" href="${esc(next.link)}" target="_blank" rel="noopener">Войти на урок</a>

</section>` : '<div class="box"><div class="empty">Ближайших уроков нет</div></div>'}

${unpaid.length ? `<div class="box alert row"><span>Не оплачено уроков: ${unpaid.length}</span><button class="btn sm light" data-screen="profile">Оплатить</button></div>` : ''}

<div class="box"><span class="uc" style="color:var(--mut)">Факт дня</span><p style="margin:8px 0 0">${esc(fact)}</p></div>

${about ? `<div class="box"><span class="uc" style="color:var(--mut)">Об Алине</span><p style="margin:8px 0 0">${esc(about)}</p></div>` : ''}`;

}

function testsScreen() {

if (TAKING) {

const t = (S.tests || []).find(x => x.id === TAKING);

if (t) return `<div class="sec"><h2>${esc(t.title)}</h2>

<form data-form="takeTest" data-id="${t.id}" class="sec">

${t.questions.map((q, i) => `<div class="box col" style="gap:8px"><span style="font-weight:500">${i + 1}. ${esc(q.text)}</span>

${q.options.map((o, j) => `<label style="flex-direction:row;align-items:center;gap:8px;color:var(--fg)"><input type="radio" name="q{j}" required style="width:auto">${esc(o)}</label>`).join('')}

</div>`).join('')}

<button class="btn" type="submit">Отправить ответы</button>

</form></div>`;

TAKING = null;

}

const tests = S.tests || [];

return `<div class="sec"><h2>Тесты</h2>

${tests.length ? tests.map(t => {

const a = (S.attempts || []).find(x => x.testId === t.id);

return `<div class="box row">

<div class="col" style="flex:1"><span style="font-weight:600">${esc(t.title)}</span><span class="muted">вопросов: ${t.questions.length}</span></div>

${a ? `<span class="tag">${a.score} из ${a.total}</span>` : `<button class="btn sm" data-act="start" data-id="${t.id}">Пройти</button>`}

</div>`;

}).join('') : '<div class="empty">Тестов пока нет</div>'}</div>`;

}

function profileScreen(me, unpaid) {

const paid = S.lessons.filter(l => l.studentId === me.id && l.paid).sort((a, b) => lessonTs(b) - lessonTs(a));

return `

${unpaid.length ? `<div class="box alert"><span>Есть неоплаченные уроки: ${unpaid.length}. Оплатить можно ниже.</span></div>` : '<div class="box"><span class="muted">Все уроки оплачены</span></div>'}

<details class="box" open>

<summary>Профиль</summary>

<form data-form="profile" data-id="${me.id}" class="sec" style="margin-top:12px">

<label>ФИО<input name="name" value="${esc(me.name)}" required></label>

<label>Телефон<input name="phone" type="tel" value="${esc(me.phone || '')}"></label>

<label>Почта<input name="email" type="email" value="${esc(me.email || '')}"></label>

<label>Класс<input name="grade" value="${esc(me.grade || '')}"></label>

<label>Цель подготовки<input name="goal" value="${esc(me.goal || '')}"></label>

<label>Username в Telegram<input value="${esc(me.username || '')}" disabled></label>

<button class="btn light" type="submit">Сохранить профиль</button>

</form>

</details>

<div class="sec"><h2>Оплата</h2>

${unpaid.map(l => `<div class="box row alert"><div class="col"><span style="font-weight:600">${shortDate(l.date)} · ${l.time} · {money(me.price)}</span></div><button class="btn sm" data-act="pay" data-id="${l.id}">Оплатить</button></div>`).join('') || '<div

class="empty">Неоплаченных уроков нет</div>'}

<span class="uc" style="color:var(--mut)">История оплат</span>

${paid.map(l => `<div class="box row"><span>${shortDate(l.date)} · {money(me.price)}</span></div>`).join('') || '<div class="empty">Оплат пока нет</div>'}

</div>`;

}

function calendarScreen(lessons) {

return `<div class="sec"><h2>Календарь</h2>

${lessons.length ? lessons.map(l => {

const past = lessonTs(l) < Date.now();

return `

<div class="box col ${past ? 'done' : ''}" style="gap:8px">

<div class="row"><span style="font-weight:600">${dayLabel(l.date)} · ${l.time}</span><span class="tag {past ? 'Прошёл' : (l.paid ? 'Оплачен' : 'Не оплачен')}</span></div>

<span>${esc(l.topic)}</span>

${past ? '' : `<a class="btn sm" href="${esc(l.link)}" target="_blank" rel="noopener">Войти на урок</a>`}

${(l.materials || []).length ? `<div class="mat"><span class="muted">Материалы</span>${l.materials.map(m => `<a href="{esc(m.title)}</a>`).join('')}</div>` : ''}

${l.homework ? `<div><span class="muted">Домашка${l.homeworkDue ? ' · до ' + shortDate(l.homeworkDue) : ''}</span><div style="white-space:pre-line">${esc(l.homework)}</div></div>` : ''}

</div>`;

}).join('') : '<div class="empty">На эти даты уроков нет</div>'}</div>`;

}

function scriptsScreen(lessons) {

return `<div class="sec"><h2>Скрипты уроков</h2>

${lessons.length ? lessons.map(l => `

<div class="box col" style="gap:6px"><span style="font-weight:600">${shortDate(l.date)} · {esc(l.script)}</div></div>`).join('')

: '<div class="empty">Скриптов пока нет. Алина добавит их к урокам.</div>'}</div>`;

}

function hwScreen(me, mine) {

const labels = { new: 'не выполнено', done: 'выполнено', checked: 'проверено' };

const standalone = S.homework.filter(h => h.studentId === me.id)

.sort((a, b) => (a.status === 'checked') - (b.status === 'checked') || a.due.localeCompare(b.due));

const lessonHw = sortLessons(mine.filter(l => l.homework));

return `<div class="sec"><h2>Домашка</h2>

${standalone.map(h => `

<div class="box row ${h.status === 'new' ? 'alert' : 'done'}">

<div class="col" style="flex:1"><span class="title">${esc(h.text)}</span><span class="muted">до ${shortDate(h.due)} · ${labels[h.status]}</span></div>

${h.status === 'checked' ? '<span class="tag">Проверено</span>' : `<button class="check {h.id}" aria-label="Отметить выполненным"></button>`}

</div>`).join('')}

${lessonHw.map(l => `

<div class="box col"><span class="muted">${shortDate(l.date)} · {l.homeworkDue ? ' · до ' + shortDate(l.homeworkDue) : ''}</span><div style="white-space:pre-line">${esc(l.homework)}</div></div>`).join('')}

${!standalone.length && !lessonHw.length ? '<div class="empty">Домашки пока нет</div>' : ''}

</div>`;

}

function tutorView() {

const today = todayIso();

const todays = sortLessons(S.lessons.filter(l => l.date === today));

const unpaidCount = S.lessons.filter(l => !l.paid && l.date >= today).length;

const toCheck = S.homework.filter(h => h.status === 'done');

const studentOpts = S.students.map(s => `<option value="${s.id}">${esc(s.name)} · ${s.exam}</option>`).join('');

return `

<div class="col">

<span class="uc" style="color:var(--mut)">${dayLabel(today)}</span>

<h1 style="font-size:34px;margin-top:4px">Сегодня</h1>

</div>

<div class="grid2">

<div class="box"><div class="display" style="font-size:28px">${todays.length}</div><span class="muted">уроков сегодня</span></div>

<div class="box {unpaidCount}</div><span class="muted">неоплаченных</span></div>

</div>

<div class="sec">

<h2>Уроки сегодня</h2>

${todays.length ? todays.map(l => {

const st = studentOf(l.studentId);

return `

<div class="box col" style="gap:10px">

<div class="row">

<div class="col">

<span class="display" style="font-size:20px">${l.time}</span>

<span style="font-weight:600">${st ? esc(st.name) : '—'} · ${esc(l.topic)}</span>

</div>

<button class="btn sm {l.id}">${l.paid ? 'Оплачен' : 'Отметить оплату'}</button>

</div>

<button class="btn sm line" data-act="copy" data-id="${l.id}" style="width:100%">Скопировать ссылку на урок</button>

<details>

<summary>Сценарий урока</summary>

<textarea data-script="${l.id}" rows="5" placeholder="План и вопросы к уроку">${esc(l.script || '')}</textarea>

</details>

</div>`;

}).join('') : '<div class="empty">Сегодня уроков нет</div>'}

</div>

<div class="sec">

<h2>Домашка на проверку <span class="muted">${toCheck.length}</span></h2>

${toCheck.length ? toCheck.map(h => {

const st = studentOf(h.studentId);

return `

<div class="box row alert">

<div class="col" style="flex:1">

<span style="font-weight:600">${st ? esc(st.name) : '—'}</span>

<span class="muted">${esc(h.text)}</span>

</div>

<button class="btn sm light" data-act="checked" data-id="${h.id}">Проверено</button>

</div>`;

}).join('') : '<div class="empty">Всё проверено</div>'}

</div>

<div class="sec">

<h2>Ученики <span class="muted">${S.students.length}</span></h2>

${S.students.map(s => {

const lessons = sortLessons(S.lessons.filter(l => l.studentId === s.id));

const hw = S.homework.filter(h => h.studentId === s.id);

const labels = { new: 'не выполнено', done: 'выполнено', checked: 'проверено' };

return `

<details class="box">

<summary class="row">

<span>${esc(s.name)}</span>

<span class="muted">${s.exam} · ${money(s.price)} · {s.topics.length}</span>

</summary>

<div class="sec" style="margin-bottom:14px">

<span class="uc" style="color:var(--mut)">Профиль</span>

<form data-form="editStudent" data-id="${s.id}" class="sec">

<label>Имя<input name="name" value="${esc(s.name)}" required></label>

<div class="grid2">

<label>Экзамен<select name="exam">${['ОГЭ', 'ЕГЭ', 'Школа'].map(x => `<option {x}</option>`).join('')}</select></label>

<label>Цена, ₽<input type="number" name="price" value="${s.price}" min="0" required></label>

</div>

<label>Username в Telegram (без @)<input name="username" value="${esc(s.username || '')}" placeholder="пусто, если ещё нет"></label>

<button class="btn sm light" type="submit" style="width:100%">Сохранить профиль</button>

</form>

<button class="btn sm line" style="width:100%" data-act="delStudent" data-id="${s.id}">Удалить ученика</button>

</div>

<div class="sec" style="margin-bottom:14px">

<span class="uc" style="color:var(--mut)">Уроки</span>

${lessons.length ? lessons.map(l => `

<div class="row" style="border-top:1px solid var(--line);padding-top:10px">

<span>${shortDate(l.date)} · ${l.time} · ${esc(l.topic)}</span>

<span class="row" style="gap:6px">

<button class="btn sm {l.id}">${l.paid ? 'Оплачен' : 'Не оплачен'}</button>

<button class="btn sm line" data-act="delLesson" data-id="${l.id}">×</button>

</span>

</div>

<details><summary class="muted">Материалы, скрипт, ДЗ</summary>

<form data-form="lessonEdit" data-id="${l.id}" class="sec" style="margin-top:10px">

<label>Материалы (по строке: название | ссылка)<textarea name="materials" rows="3">${esc(matText(l))}</textarea></label>

<label>Скрипт урока<textarea name="script" rows="3">${esc(l.script || '')}</textarea></label>

<label>Домашка<textarea name="homework" rows="2">${esc(l.homework || '')}</textarea></label>

<label>Срок домашки<input type="date" name="homeworkDue" value="${l.homeworkDue || ''}"></label>

<button class="btn sm light" type="submit" style="width:100%">Сохранить урок</button>

</form>

</details>`).join('') : '<div class="empty">Уроков нет</div>'}

</div>

<div class="sec" style="margin-bottom:14px">

<span class="uc" style="color:var(--mut)">Домашка</span>

${hw.length ? hw.map(h => `

<div class="row" style="border-top:1px solid var(--line);padding-top:10px">

<span style="flex:1">${esc(h.text)} <span class="muted">· до ${shortDate(h.due)} · ${labels[h.status]}</span></span>

<button class="btn sm line" data-act="copyhw" data-id="${h.id}">Копировать</button>

<button class="btn sm line" data-act="delHw" data-id="${h.id}">×</button>

</div>`).join('') : '<div class="empty">Заданий нет</div>'}

</div>

<div class="sec">

<span class="uc" style="color:var(--mut)">Прогресс · темы</span>

${s.topics.map((t, i) => `

<button class="topic" data-act="topic" data-s="${s.id}" data-i="${i}">

<span>${esc(t)}</span>

<span class="check ${s.passed.includes(i) ? 'on' : ''}"></span>

</button>`).join('')}

</div>

</details>`;

}).join('')}

</div>

<div class="grid2">

<button class="btn sm line" style="width:100%" data-act="ics">Календарь (.ics)</button>

<button class="btn sm line" style="width:100%" data-act="hwdl">Выгрузить домашку</button>

</div>

<button class="btn sm line" style="width:100%" data-act="perm">Включить напоминания</button>

<div class="sec"><h2>Тесты</h2>

${(S.tests || []).map(t => {

const att = (S.attempts || []).filter(a => a.testId === t.id);

const who = t.assign === 'all' ? 'все ученики' : (studentOf(t.assign) ? studentOf(t.assign).name : '—');

return `<div class="box col" style="gap:6px">

<div class="row"><span style="font-weight:600">{t.id}">×</button></div>

<span class="muted">${who} · вопросов: ${t.questions.length}</span>

${att.map(a => `<span>${esc(studentOf(a.studentId) ? studentOf(a.studentId).name : '—')}: ${a.score} из ${a.total}</span>`).join('') || '<span class="muted">Ответов пока нет</span>'}

</div>`;

}).join('') || '<div class="empty">Тестов пока нет</div>'}

</div>

<details class="box">

<summary>+ Создать тест</summary>

<form data-form="test" class="sec" style="margin-top:12px">

<label>Название<input name="title" required></label>

<label>Кому<select name="assign"><option value="all">Всем ученикам</option>${S.students.map(x => `<option value="${x.id}">${esc(x.name)}</option>`).join('')}</select></label>

<label>Вопросы, одна строка на вопрос: вопрос | вариант | вариант | номер верного<textarea name="questions" rows="6" required placeholder="Какой год битва при Марафоне? | 480 | 490 | 500 | 2"></textarea></label>

<button class="btn" type="submit">Создать тест</button>

</form>

</details>

<details class="box">

<summary>Факты о преподавателе</summary>

<form data-form="facts" class="sec" style="margin-top:12px">

<label>По одному факту на строку<textarea name="facts" rows="5">${esc((S.teacherFacts || []).join('\n'))}</textarea></label>

<button class="btn" type="submit">Сохранить</button>

</form>

</details>

<details class="box">

<summary>+ Создать урок</summary>

<form data-form="lesson" class="sec">

<label>Ученик<select name="studentId" required>${studentOpts}</select></label>

<div class="grid2">

<label>Дата<input type="date" name="date" value="${today}" required></label>

<label>Время<input type="time" name="time" value="18:00" required></label>

</div>

<label>Длительность

<select name="dur"><option value="60">60 минут</option><option value="90">90 минут</option></select>

</label>

<label>Тема<input type="text" name="topic" placeholder="Например, Средние века"></label>

<label>Сценарий урока<textarea name="script" rows="3" placeholder="План, вопросы, материалы"></textarea></label>

<button class="btn" type="submit">Создать урок</button>

</form>

</details>

<details class="box">

<summary>+ Создать домашку</summary>

<form data-form="hw" class="sec">

<label>Ученик<select name="studentId" required>${studentOpts}</select></label>

<label>Задание<textarea name="text" rows="3" required placeholder="Текст задания"></textarea></label>

<label>Срок<input type="date" name="due" value="${addDays(3)}" required></label>

<button class="btn" type="submit">Задать</button>

</form>

</details>

<details class="box">

<summary>+ Добавить ученика</summary>

<form data-form="student" class="sec">

<label>Имя<input type="text" name="name" required></label>

<div class="grid2">

<label>Экзамен<select name="exam"><option>ОГЭ</option><option>ЕГЭ</option><option>Школа</option></select></label>

<label>Username в Telegram<input type="text" name="username" placeholder="без @"></label>

<label>Цена урока, ₽<input type="number" name="price" value="1500" min="0" required></label>

</div>

<button class="btn" type="submit">Добавить</button>

</form>

</details>`;

}

function registerView() {

return `

<div class="col"><h1 style="font-size:28px">Регистрация</h1><span class="muted">Заполните анкету, профиль создастся автоматически.</span></div>

<form data-form="register" class="card" style="color:var(--fg)">

<label style="color:var(--bg)">ФИО<input name="name" required placeholder="Иванова Анна Петровна"></label>

<label style="color:var(--bg)">Класс

<select name="grade"><option>5</option><option>6</option><option>7</option><option>8</option><option>9</option><option>10</option><option>11</option><option>Выпускник</option></select>

</label>

<label style="color:var(--bg)">Цель подготовки

<select name="goal"><option>Школьная программа</option><option>ОГЭ</option><option>ЕГЭ</option></select>

</label>

<label style="color:var(--bg)">Телефон<input name="phone" type="tel" placeholder="+7…"></label>

<label style="color:var(--bg)">Почта<input name="email" type="email" placeholder="name@mail.ru"></label>

<button class="btn" type="submit">Создать профиль</button>

</form>`;

}

function render() {

if (NEW) {

document.querySelector('.tabs').style.display = 'none';

document.getElementById('who').textContent = '';

document.getElementById('app').innerHTML = registerView();

return;

}

if (GUEST) {

document.querySelector('.tabs').style.display = 'none';

document.getElementById('who').textContent = '';

document.getElementById('app').innerHTML = '<div class="box"><div class="empty">Вас нет в списке учеников. Напишите репетитору, он добавит вас.</div></div>';

return;

}

document.querySelector('.tabs').style.display = ROLE === 'tutor' ? '' : 'none';

document.querySelectorAll('.tabs button').forEach(b => b.classList.toggle('on', b.dataset.role === S.role));

document.getElementById('who').textContent = S.role === 'tutor' ? 'Репетитор' : 'Ученик';

document.getElementById('app').innerHTML = S.role === 'tutor' ? tutorView() : studentView();

}

const pad = n => String(n).padStart(2, '0');

async function saveFile(name, text, mime) {

try {

const dl = await window.claude.use('downloads');

if (dl) { await dl.save({ filename: name, data: text }); return; }

} catch (e) {}

if (TG) {

try { await navigator.clipboard.writeText(text); toast('Скопировано, вставьте в чат'); }

catch (e) { toast('Не удалось скопировать'); }

return;

}

const a = document.createElement('a');

a.href = URL.createObjectURL(new Blob([text], { type: mime }));

a.download = name;

a.click();

}

function calendarText(lessons) {

const f = d => d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + 'T' + pad(d.getHours()) + pad(d.getMinutes()) + '00';

const out = ['BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//VSE USPET//RU', 'CALSCALE:GREGORIAN'];

lessons.forEach(l => {

const st = studentOf(l.studentId);

const start = new Date(l.date + 'T' + l.time + ':00');

const end = new Date(start.getTime() + l.dur * 60000);

out.push('BEGIN:VEVENT', 'UID:' + l.id + '@vse-uspet', 'DTSTAMP:' + f(new Date()), 'DTSTART:' + f(start), 'DTEND:' + f(end),

'SUMMARY:' + (st ? st.name + ' · ' : '') + l.topic, 'DESCRIPTION:Ссылка: ' + l.link,

'BEGIN:VALARM', 'ACTION:DISPLAY', 'DESCRIPTION:Урок через 2 часа', 'TRIGGER:-PT120M', 'END:VALARM',

'BEGIN:VALARM', 'ACTION:DISPLAY', 'DESCRIPTION:Урок через 1 час', 'TRIGGER:-PT60M', 'END:VALARM', 'END:VEVENT');

});

out.push('END:VCALENDAR');

return out.join('\r\n');

}

function homeworkText() {

const label = { new: 'не выполнено', done: 'выполнено', checked: 'проверено' };

return S.students.map(s => {

const hw = S.homework.filter(h => h.studentId === s.id);

return s.name + ' (' + s.exam + ')\n' + (hw.length ? hw.map(h => '— ' + h.text + ' · до ' + h.due + ' · ' + label[h.status]).join('\n') : '— заданий нет');

}).join('\n\n');

}

function notify(text) {

if ('Notification' in window && Notification.permission === 'granted') new Notification('ВСЕ УСПЕТЬ', { body: text });

else toast(text);

}

const notified = new Set();

function remind() {

const now = Date.now();

const list = S.role === 'tutor' ? S.lessons : S.lessons.filter(l => l.studentId === S.me);

list.forEach(l => {

const mins = Math.round((lessonTs(l) - now) / 60000);

[120, 60].forEach(m => {

const key = l.id + ':' + m;

if (mins <= m && mins > m - 2 && !notified.has(key)) {

notified.add(key);

let text = 'Через ' + (m === 60 ? '1 час' : '2 часа') + ' урок в ' + l.time + '. Ссылка в приложении';

if (!l.paid) text += '. Урок ещё не оплачен, оплатить можно в приложении';

notify(text);

}

});

});

}

document.addEventListener('click', async e => {

const scr = e.target.closest('[data-screen]');

if (scr) { SCREEN = scr.dataset.screen; render(); window.scrollTo(0, 0); return; }

const roleBtn = e.target.closest('[data-role]');

if (roleBtn) { S.role = roleBtn.dataset.role; save(); render(); return; }

const t = e.target.closest('[data-act]');

if (!t) return;

const act = t.dataset.act;

const id = t.dataset.id;

if (act === 'pay') {

try {

const j = await api('invoice', { lessonId: id });

if (j.error || !j.url) { toast('Оплата сейчас недоступна, попробуйте позже'); return; }

if (TG && TG.openInvoice) TG.openInvoice(j.url, status => { if (status === 'paid') { toast('Оплачено'); pull(); } });

else window.open(j.url, '_blank');

} catch (err) { toast('Нет связи, попробуйте позже'); }

return;

} else if (act === 'paytoggle') {

const l = S.lessons.find(x => x.id === id); if (l) l.paid = !l.paid;

} else if (act === 'hw') {

const h = S.homework.find(x => x.id === id);

if (h && h.status !== 'checked') h.status = h.status === 'new' ? 'done' : 'new';

} else if (act === 'checked') {

const h = S.homework.find(x => x.id === id); if (h) h.status = 'checked';

toast('Проверено');

} else if (act === 'topic') {

const s = studentOf(t.dataset.s); const i = Number(t.dataset.i);

if (s) s.passed = s.passed.includes(i) ? s.passed.filter(x => x !== i) : [...s.passed, i];

} else if (act === 'ics') {

const list = S.role === 'tutor' ? S.lessons : S.lessons.filter(l => l.studentId === S.me);

await saveFile('lessons.ics', calendarText(sortLessons(list)), 'text/calendar');

return;

} else if (act === 'hwdl') {

await saveFile('homework.txt', homeworkText(), 'text/plain');

return;

} else if (act === 'perm') {

if ('Notification' in window) {

const r = await Notification.requestPermission();

toast(r === 'granted' ? 'Напоминания включены' : 'Браузер не разрешил уведомления');

} else toast('Уведомления недоступны в этом браузере');

return;

} else if (act === 'delStudent') {

if (!confirm('Удалить ученика вместе с его уроками и домашкой?')) return;

S.students = S.students.filter(x => x.id !== id);

S.lessons = S.lessons.filter(x => x.studentId !== id);

S.homework = S.homework.filter(x => x.studentId !== id);

} else if (act === 'delLesson') {

S.lessons = S.lessons.filter(x => x.id !== id);

} else if (act === 'delHw') {

S.homework = S.homework.filter(x => x.id !== id);

} else if (act === 'copyhw') {

const h = S.homework.find(x => x.id === id);

const st = h && studentOf(h.studentId);

const text = (st ? st.name + ', ' : '') + 'домашка: ' + h.text + '. Срок: ' + shortDate(h.due) + '.';

try { await navigator.clipboard.writeText(text); toast('Скопировано, вставьте в чат'); }

catch (err) { toast('Не удалось скопировать'); }

return;

} else if (act === 'start') {

TAKING = id; SCREEN = 'tests'; render(); window.scrollTo(0, 0); return;

} else if (act === 'delTest') {

S.tests = (S.tests || []).filter(x => x.id !== id);

} else if (act === 'copy') {

const l = S.lessons.find(x => x.id === id);

try { await navigator.clipboard.writeText(l.link); toast('Ссылка скопирована'); }

catch (err) { toast(l.link); }

return;

}

save(); render();

});

document.addEventListener('change', e => {

if (e.target.matches('[data-script]')) {

const l = S.lessons.find(x => x.id === e.target.dataset.script);

if (l) { l.script = e.target.value; save(); toast('Сценарий сохранён'); }

return;

}

if (e.target.matches('[data-field="me"]')) { S.me = e.target.value; save(); render(); }

});

document.addEventListener('submit', e => {

const form = e.target.closest('[data-form]');

if (!form) return;

e.preventDefault();

const f = new FormData(form);

const kind = form.dataset.form;

if (kind === 'lesson') {

S.lessons.push({ id: uid(), date: f.get('date'), time: f.get('time'), dur: Number(f.get('dur')),

studentId: f.get('studentId'), topic: (f.get('topic') || '').trim() || 'Без темы', script: String(f.get('script') || '').trim(), paid: false,

link: TL + Math.floor(1e10 + Math.random() * 9e10) });

} else if (kind === 'hw') {

S.homework.push({ id: uid(), studentId: f.get('studentId'), text: String(f.get('text')).trim(),

due: f.get('due'), status: 'new' });

} else if (kind === 'register') {

api('register', { profile: { name: f.get('name'), grade: f.get('grade'), goal: f.get('goal'), phone: f.get('phone'), email: f.get('email') } })

.then(j => { if (j.error) { toast(j.error === 'need_username' ? 'Сначала задайте username в Telegram' : 'Заполните ФИО'); return; } NEW = false; applyView(j.state, 'student'); lastRemote = JSON.stringify(j.state); render(); toast('Профиль создан'); });

return;

} else if (kind === 'profile') {

const st = studentOf(form.dataset.id);

if (st) { st.name = String(f.get('name')).trim(); st.phone = f.get('phone'); st.email = f.get('email'); st.grade = f.get('grade'); st.goal = f.get('goal'); }

} else if (kind === 'lessonEdit') {

const l = S.lessons.find(x => x.id === form.dataset.id);

if (l) { l.materials = parseMat(f.get('materials')); l.script = String(f.get('script') || '').trim(); l.homework = String(f.get('homework') || '').trim(); l.homeworkDue = f.get('homeworkDue') || ''; }

} else if (kind === 'takeTest') {

const t = (S.tests || []).find(x => x.id === form.dataset.id);

const answers = t ? t.questions.map((_, i) => Number(f.get('q' + i))) : [];

api('submitTest', { testId: form.dataset.id, answers }).then(j => {

if (j.error) { toast(j.error === 'done' ? 'Тест уже пройден' : 'Не удалось отправить'); return; }

applyView(j.state, 'student'); lastRemote = JSON.stringify(j.state); TAKING = null;

toast('Результат: ' + j.result.score + ' из ' + j.result.total); render();

});

return;

} else if (kind === 'test') {

const questions = String(f.get('questions') || '').split('\n').map(x => x.split('|').map(y => y.trim())).map(parts => {

const correct = parseInt(parts[parts.length - 1], 10) - 1;

const options = parts.slice(1, -1).filter(Boolean);

return parts[0] && options.length >= 2 && correct >= 0 && correct < options.length ? { text: parts[0], options, correct } : null;

}).filter(Boolean);

if (!questions.length) { toast('Нет корректных вопросов. Формат: вопрос | вариант | вариант | номер верного'); return; }

S.tests = [...(S.tests || []), { id: uid(), title: String(f.get('title')).trim(), assign: f.get('assign'), questions }];

} else if (kind === 'facts') {

S.teacherFacts = String(f.get('facts') || '').split('\n').map(x => x.trim()).filter(Boolean);

} else if (kind === 'editStudent') {

const st = studentOf(form.dataset.id);

if (st) {

const exam = f.get('exam');

const newTopics = exam === 'ЕГЭ' ? EGE_TOPICS : (exam === 'ОГЭ' ? OGE_TOPICS : SCHOOL_TOPICS);

if (exam !== st.exam) { st.passed = []; st.topics = newTopics; }

st.name = String(f.get('name')).trim();

st.exam = exam;

st.price = Number(f.get('price'));

st.username = String(f.get('username') || '').replace('@', '').trim();

}

} else if (kind === 'student') {

const exam = f.get('exam');

S.students.push({ id: 's' + uid(), name: String(f.get('name')).trim(), exam, price: Number(f.get('price')),

username: String(f.get('username') || '').replace('@', '').trim(),

topics: exam === 'ЕГЭ' ? EGE_TOPICS : (exam === 'ОГЭ' ? OGE_TOPICS : SCHOOL_TOPICS), passed: [] });

}

save(); render(); toast('Сохранено');

});

render();

pull();

setInterval(() => pull(), 15000);

setInterval(remind, 60000);

remind();

</script>

</body>

</html>