feat(biochem): Фаза 4 (срез) — персистентность прогресса путей + награда
Learn-режим метаболических путей теперь сохраняет прохождение на пользователя (раньше прогресс терялся). - migration 044_bio_user_pathway: таблица bio_user_pathway(user_id, pathway, step, completed) с upsert. - biochemController: getPathwayProgress / savePathwayProgress; XP (+80) начисляется один раз при первом завершении пути (completed «липкий» через MAX), затем checkAchievements. Роуты GET/POST /biochem/pathways/progress. - js/api.js: biochemGetPathwayProgress / biochemSavePathwayProgress. - biochem-pathways.html: загрузка прогресса в init (галочка-SVG на пройденных путях), сохранение + тост «+XP» при завершении пути. Полный перенос данных путей в БД (4.1-4.3) отложен — хардкод путей работает, ценность миграции архитектурная; здесь доставлена пользовательская часть. Проверено: upsert, XP-once, completed-sticky на реальной БД. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1060,6 +1060,35 @@ function clickNode(id) {
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// LEARN MODE
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// ── Прогресс прохождения путей (персистентность Learn-режима) ──
|
||||
let _pathProgress = {};
|
||||
async function loadPathProgress() {
|
||||
try { _pathProgress = (await LS.biochemGetPathwayProgress()) || {}; }
|
||||
catch { _pathProgress = {}; }
|
||||
markCompletedChips();
|
||||
}
|
||||
function markCompletedChips() {
|
||||
document.querySelectorAll('.path-chip').forEach(chip => {
|
||||
const key = chip.dataset.path;
|
||||
const done = _pathProgress[key] && _pathProgress[key].completed;
|
||||
let badge = chip.querySelector('.path-done');
|
||||
if (done && !badge) {
|
||||
badge = document.createElement('span');
|
||||
badge.className = 'path-done';
|
||||
badge.style.cssText = 'display:inline-flex;margin-left:5px;color:#4ade80';
|
||||
badge.innerHTML = '<svg class="ic" viewBox="0 0 24 24" style="width:12px;height:12px"><polyline points="20 6 9 17 4 12"/></svg>';
|
||||
chip.appendChild(badge);
|
||||
} else if (!done && badge) { badge.remove(); }
|
||||
});
|
||||
}
|
||||
function savePathCompletion() {
|
||||
LS.biochemSavePathwayProgress(currentPath, learnStep, true).then(r => {
|
||||
_pathProgress[currentPath] = { step: learnStep, completed: true };
|
||||
markCompletedChips();
|
||||
if (r && r.xp) LS.toast(`Путь пройден! +${r.xp} XP`, 'success');
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function startLearn() {
|
||||
learnMode = true;
|
||||
learnStep = 0;
|
||||
@@ -1149,6 +1178,7 @@ function stepNav(dir) {
|
||||
|
||||
function renderLearnComplete() {
|
||||
activeNode = null;
|
||||
savePathCompletion(); // сохранить прохождение + начислить XP (один раз)
|
||||
renderNodes(PATHWAYS[currentPath]);
|
||||
document.getElementById('learn-active').innerHTML = `
|
||||
<div class="learn-complete">
|
||||
@@ -1210,10 +1240,8 @@ svgArea.addEventListener('wheel', e => {
|
||||
async function init() {
|
||||
try {
|
||||
const user = await LS.getMe();
|
||||
const initials = (user?.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
|
||||
document.getElementById('nav-avatar').textContent = initials;
|
||||
const nav2 = document.getElementById('nav-avatar2');
|
||||
if (nav2) nav2.textContent = initials;
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar2'), user);
|
||||
if (user?.role === 'admin') document.getElementById('btn-admin').style.display = '';
|
||||
LS.applyRoleSidebar(user);
|
||||
LS.showBoardIfAllowed();
|
||||
@@ -1227,6 +1255,7 @@ async function init() {
|
||||
await new Promise(r => setTimeout(r, 60));
|
||||
renderPath();
|
||||
renderPathInfo();
|
||||
loadPathProgress(); // отметить пройденные пути галочкой
|
||||
if (window.lucide) lucide.createIcons();
|
||||
LS.notif?.init();
|
||||
LS.hideDisabledFeatures?.();
|
||||
|
||||
Reference in New Issue
Block a user