/* ===== Student cabinet: dashboard, course player, test ===== */ const D4 = window.DATA; function lessonTitle(lesson){ return typeof lesson==='string' ? lesson : (lesson?.t || 'Урок'); } function lessonVideo(lesson){ return typeof lesson==='object' && lesson ? lesson.video : ''; } function lessonFile(lesson){ return typeof lesson==='object' && lesson ? lesson.file : ''; } function isPdfFile(path){ return /\.pdf(?:[#?].*)?$/i.test(String(path || '')); } function courseMediaUrl(accessCourseId,path){ const value=String(path||''); const match=value.match(/^uploads\/courses\/([a-z0-9][a-z0-9-]{0,79})\/(.+)$/i); if(!match) return value; const mediaCourseId=match[1]; const accessParam=mediaCourseId===String(accessCourseId||'') ? '' : `&access_course_id=${encodeURIComponent(accessCourseId)}`; return `api/course-media.php?course_id=${encodeURIComponent(mediaCourseId)}&path=${encodeURIComponent(match[2])}${accessParam}`; } function PdfLessonViewer({ src, title }){ const canvasRef=useRef(null); const shellRef=useRef(null); const documentRef=useRef(null); const renderTaskRef=useRef(null); const [pageNumber,setPageNumber]=useState(1); const [pageCount,setPageCount]=useState(0); const [loading,setLoading]=useState(true); const [error,setError]=useState(''); useEffect(()=>{ let cancelled=false; setLoading(true); setError(''); setPageNumber(1); setPageCount(0); documentRef.current=null; if(!window.pdfjsLib){ setLoading(false); setError('Модуль просмотра PDF не загрузился. Обновите страницу.'); return ()=>{}; } window.pdfjsLib.GlobalWorkerOptions.workerSrc='assets/vendor/pdfjs/pdf.worker.min.js'; const task=window.pdfjsLib.getDocument({url:src,withCredentials:true}); task.promise.then(pdf=>{ if(cancelled) return; documentRef.current=pdf; setPageCount(pdf.numPages); setLoading(false); }).catch(err=>{ if(cancelled) return; setLoading(false); setError(err?.message || 'Не удалось открыть PDF.'); }); return ()=>{ cancelled=true; documentRef.current=null; task.destroy(); }; },[src]); useEffect(()=>{ const pdf=documentRef.current; const canvas=canvasRef.current; if(!pdf || !canvas || loading || error) return; let cancelled=false; pdf.getPage(pageNumber).then(page=>{ if(cancelled) return; const baseViewport=page.getViewport({scale:1}); const availableWidth=Math.max(280,Math.min(960,(shellRef.current?.clientWidth||900)-32)); const scale=availableWidth/baseViewport.width; const viewport=page.getViewport({scale}); const pixelRatio=Math.min(window.devicePixelRatio||1,2); const context=canvas.getContext('2d',{alpha:false}); canvas.width=Math.floor(viewport.width*pixelRatio); canvas.height=Math.floor(viewport.height*pixelRatio); canvas.style.width=`${Math.floor(viewport.width)}px`; canvas.style.height=`${Math.floor(viewport.height)}px`; renderTaskRef.current=page.render({ canvasContext:context, viewport, transform:pixelRatio===1?null:[pixelRatio,0,0,pixelRatio,0,0], }); return renderTaskRef.current.promise; }).catch(err=>{ if(!cancelled && err?.name!=='RenderingCancelledException'){ setError(err?.message || 'Не удалось отобразить страницу PDF.'); } }); return ()=>{ cancelled=true; renderTaskRef.current?.cancel(); renderTaskRef.current=null; }; },[pageNumber,pageCount,loading,error]); return (
e.preventDefault()} style={{border:'1px solid rgba(10,60,80,.12)',borderRadius:18,background:'#eef3f4',overflow:'hidden'}}>
{loading?'Загрузка PDF…':error?'Ошибка просмотра':`${title} · страница ${pageNumber} из ${pageCount}`} {!loading && !error && pageCount>1 &&
}
{loading &&
Подготавливаем материал урока…
} {error &&
{error}
}
); } function CabShell({ active, go, onLogout, children, user }){ const [menuOpen,setMenuOpen]=useState(false); const isLeagueMember = !isStaffRole(user?.role) && !!(user?.league_member || user?.leagueMember); const leagueItems = [ { k:'league-rating', t:'Рейтинг', i:'trophy' }, { k:'league-infrastructure', t:'Инфраструктура', i:'pin' }, { k:'league-discounts', t:'Скидки', i:'star' }, { k:'league-pilot-card', t:'Спортивная карточка', i:'award' }, { k:'league-support', t:'Техподдержка', i:'wrench' }, ]; const studentItems = [ { k:'cabinet', t:'Мои курсы', i:'book' }, { k:'cab-materials', t:'Материалы', i:'doc' }, { k:'cab-certs', t:'Сертификаты', i:'award' }, ]; const items = [ ...(isLeagueMember ? leagueItems : studentItems), { k:'cab-settings', t:'Настройки', i:'settings' }, ]; function navTo(k){ go(k); setMenuOpen(false); } return (
setMenuOpen(false)}/>
go('home')}> Личный кабинет
{children}
); } function Dashboard({ go, enroll, user }){ const data = enroll.map(e=>({ ...e, course: D4.courses.find(c=>c.id===e.id) })).filter(x=>x.course); const active = data.filter(d=>d.access==='open'); const done = data.filter(d=>d.progress===100).length; const avg = (()=>{ const s=data.filter(d=>d.testScore!=null); return s.length?Math.round(s.reduce((a,b)=>a+b.testScore,0)/s.length):'—'; })(); return (

Привет, {user?.name || 'студент'} 👋

Продолжайте обучение с того места, где остановились.

{active.length}активных курсов
{done}завершено
{avg}{avg!=='—'?'%':''}средний балл
14чв этом месяце

Мои курсы

{data.map(d=>())}
); } function EnrollRow({ d, go }){ const c=d.course; const league = D4.leagueMembership || {}; const displayTitle = c.id==='racing' && league.title ? league.title : c.title; const isLeague = c.id==='racing'; const locked = d.access!=='open'; return (
{displayTitle} {d.status==='Завершён' && Завершён} {d.status==='В процессе' && В процессе} {locked && {d.status}}
{!locked ? ( isLeague ?
Активно {d.leaguePlan && {d.leaguePlan}} Действует до {d.validUntil || '—'}
:
{d.progress}%
) :

Доступ откроется после подтверждения оплаты администратором.

} {d.testScore!=null &&
Результат теста: {d.testScore}%
}
{locked ? : isLeague ? {d.leaguePlan || 'Членство'}
{d.validUntil ? `до ${d.validUntil}` : 'срок не указан'}
: }
); } /* ---------------- COURSE PLAYER ---------------- */ function Player({ id, go, enroll, onProgress }){ const c = D4.courses.find(x=>x.id===id)||D4.courses[0]; const e = enroll.find(x=>x.id===id)||{progress:0}; const flat=[]; c.modules.forEach((m,mi)=>m.lessons.forEach((l,li)=>flat.push({l,mi,li,t:m.t}))); const legacyCompletedCount=Math.max(0,Math.min(flat.length,Math.round(((e.progress||0)/100)*flat.length))); const initialCompletedLessons=Array.isArray(e.completedLessons) ? [...new Set(e.completedLessons.filter(index=>Number.isInteger(index)&&index>=0&&indexa-b) : Array.from({length:legacyCompletedCount},(_,index)=>index); const initialLessonIndex=initialCompletedLessons.lengthindex).find(index=>!initialCompletedLessons.includes(index))??0) : 0; const [cur,setCur]=useState(initialLessonIndex); const [completedLessons,setCompletedLessons]=useState(initialCompletedLessons); const [progressError,setProgressError]=useState(''); const [finishing,setFinishing]=useState(false); const lesson=flat[cur]; const rawMaterialSrc = lessonFile(lesson.l); const rawVideoSrc = lessonVideo(lesson.l); const materialSrc = courseMediaUrl(c.id,rawMaterialSrc); const videoSrc = courseMediaUrl(c.id,rawVideoSrc); const materialIsPdf = isPdfFile(rawMaterialSrc); const lessonAssetMatch=String(rawMaterialSrc||'').match(/^(uploads\/courses\/[^/]+\/lesson-\d+)\/presentation\.pdf$/i); const lessonAssetBase=lesson.l?.assetBase||lessonAssetMatch?.[1]||''; const explicitMaterials=Array.isArray(lesson.l?.materials)?lesson.l.materials:null; const materialEnabled=key=>explicitMaterials===null||explicitMaterials.includes(key); const defaultMaterialKey=materialEnabled('pdf')&&materialSrc&&materialIsPdf ? 'pdf' : materialEnabled('docx')&&lessonAssetBase ? 'docx' : null; const lessonMaterials=[ materialEnabled('pdf') && materialSrc && materialIsPdf ? {key:'pdf',label:'PDF',icon:'doc',kind:'pdf',src:materialSrc,title:'Презентация PDF'} : null, materialEnabled('audio') && lessonAssetBase ? {key:'audio',label:'Аудио',icon:'play',kind:'audio',src:courseMediaUrl(c.id,`${lessonAssetBase}/audio.m4a`),title:'Аудиолекция'} : null, materialEnabled('pptx') && lessonAssetBase ? {key:'pptx',label:'PPTX',icon:'layers',kind:'pdf',src:courseMediaUrl(c.id,`${lessonAssetBase}/presentation-preview.pdf`),title:'Презентация PowerPoint'} : null, materialEnabled('docx') && lessonAssetBase ? {key:'docx',label:'DOCX',icon:'doc',kind:'pdf',src:courseMediaUrl(c.id,`${lessonAssetBase}/notes-preview.pdf`),title:'Конспект DOCX'} : null, c.id==='aeronavigation-bpla-part-1' && /\/lesson-03$/i.test(lessonAssetBase) ? {key:'audio-2',label:'Доп. аудио',icon:'play',kind:'audio',src:courseMediaUrl(c.id,`${lessonAssetBase}/audio-2.m4a`),title:'Дополнительная аудиолекция'} : null, ].filter(Boolean); const [activeMaterial,setActiveMaterial]=useState(defaultMaterialKey); const selectedMaterial=lessonMaterials.find(item=>item.key===activeMaterial)||null; const isLast = cur===flat.length-1; const currentCompleted=completedLessons.includes(cur); const allLessonsCompleted=completedLessons.length===flat.length; const canFinishFromLast=isLast&&(allLessonsCompleted||(!currentCompleted&&completedLessons.length===flat.length-1)); useEffect(()=>{ setActiveMaterial(defaultMaterialKey); },[cur, defaultMaterialKey]); function goLesson(next){ const bounded = Math.max(0, Math.min(flat.length-1, next)); setCur(bounded); } function setLessonCompletion(index,completed){ setProgressError(''); setCompletedLessons(current=>{ const next=completed?[...new Set([...current,index])]:current.filter(value=>value!==index); return next.sort((a,b)=>a-b); }); const request=onProgress&&onProgress(c.id,index,flat.length,completed); if(request&&typeof request.catch==='function'){ return request.catch(error=>{ setCompletedLessons(current=>completed?current.filter(value=>value!==index):[...new Set([...current,index])].sort((a,b)=>a-b)); setProgressError(error?.message||'Не удалось сохранить прохождение урока. Обновите страницу и попробуйте ещё раз.'); throw error; }); } return Promise.resolve(); } function goNextLesson(){ if(!currentCompleted)setLessonCompletion(cur,true).catch(()=>{}); goLesson(cur+1); } async function finishAndOpenTest(){ if(!canFinishFromLast||finishing)return; setFinishing(true); try{ if(!currentCompleted)await setLessonCompletion(cur,true); go('test',c.id); }catch(error){} finally{setFinishing(false);} } return (
{c.title} Урок {cur+1} / {flat.length}
{videoSrc ? : Используйте доступный конспект урока}
{lesson.t}

{lessonTitle(lesson.l)}

В этом уроке разбираем тему на практике: ключевые понятия, демонстрация на симуляторе и чек-лист для самостоятельной отработки. Учебные материалы и конспект доступны во вкладке ниже.

{lessonMaterials.map(item=>( ))} {!lessonMaterials.length && Материалы не прикреплены} {videoSrc && Защищённое видео} Симулятор-сценарий
{selectedMaterial && (
{selectedMaterial.title}
{selectedMaterial.kind==='audio' ?
e.preventDefault()}>
: }
)}
{isLast ? : }
{isLast&&!canFinishFromLast&&
Для тестирования отметьте пройденными остальные уроки. Осталось: {flat.length-completedLessons.length}.
} {progressError&&
{progressError}
}
); } Object.assign(window, { CabShell, Dashboard, Player });