edb4c211a0
- Add js/sidebar.js: generates full sidebar HTML into #app-sidebar, handles role-based visibility, active link (with prefix matching), toggle wiring, collapsed state, board/features/notif init - Replace <aside class="sidebar">...</aside> with <aside id="app-sidebar"> across all 35 standard-layout pages via scripts/apply-sidebar.js - Add notifications.js to 5 pages that were missing it - Fix api.js initPage(): skip toggle re-wiring if data-sb-wired set, fix active link selector .sb-item → .sb-link - Remove stale sbl-*/nav-admin/btn-upload-nav getElementById calls that crashed after sidebar replacement (lab, classes, collection, crossword, hangman, knowledge-map, library, pet, profile) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1853 lines
84 KiB
HTML
1853 lines
84 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>Архив уроков — LearnSpace</title>
|
||
<link rel="icon" href="/favicon.svg" type="image/svg+xml"/>
|
||
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||
<link rel="stylesheet" href="/css/ls.css"/>
|
||
<script src="https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
||
<style>
|
||
/* ── Local token overrides — LIGHT THEME ── */
|
||
.lh-wrap {
|
||
--r: 12px;
|
||
--lh-bg: #f5f4fb;
|
||
--lh-surface: #ffffff;
|
||
--lh-surface-2: #f0eefb;
|
||
--lh-border: rgba(155,93,229,0.18);
|
||
--lh-border-h: rgba(155,93,229,0.4);
|
||
--lh-text: #1a1433;
|
||
--lh-text-2: #4a3f6b;
|
||
--lh-text-3: #8f86a8;
|
||
--lh-glow-v: rgba(155,93,229,0.15);
|
||
--lh-glow-c: rgba(6,214,224,0.1);
|
||
}
|
||
|
||
/* ── Root height fix: sb-content has no explicit height → give it one ── */
|
||
.app-layout { height: 100vh; overflow: hidden; }
|
||
.sb-content { height: 100%; overflow: hidden; }
|
||
|
||
/* ── Layout ── */
|
||
.lh-wrap {
|
||
display: flex; flex-direction: column; gap: 0;
|
||
height: 100%; overflow: hidden;
|
||
background: var(--lh-bg);
|
||
color: var(--lh-text);
|
||
}
|
||
|
||
/* ── Header ── */
|
||
.lh-header {
|
||
padding: 16px 32px 14px;
|
||
border-bottom: 1px solid var(--lh-border);
|
||
background: var(--lh-surface);
|
||
flex-shrink: 0;
|
||
}
|
||
.lh-header-top {
|
||
display: flex; align-items: center; justify-content: space-between; gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.lh-title {
|
||
font-family: 'Unbounded', sans-serif; font-size: 1.2rem;
|
||
font-weight: 800; margin: 0;
|
||
display: flex; align-items: center; gap: 12px;
|
||
background: linear-gradient(135deg, #9B5DE5, #06D6E0);
|
||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
.lh-title svg {
|
||
color: var(--violet);
|
||
-webkit-text-fill-color: initial;
|
||
filter: drop-shadow(0 0 8px rgba(155,93,229,0.4));
|
||
}
|
||
.lh-filters {
|
||
display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 10px;
|
||
}
|
||
.lh-filter-select {
|
||
padding: 8px 14px; border-radius: 10px; border: 1.5px solid var(--lh-border);
|
||
background: var(--lh-surface); color: var(--lh-text); font-family: 'Manrope', sans-serif;
|
||
font-size: 0.82rem; font-weight: 600; cursor: pointer;
|
||
transition: border-color .18s, box-shadow .18s;
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
.lh-filter-select:focus {
|
||
border-color: var(--violet); outline: none;
|
||
box-shadow: 0 0 0 3px rgba(155,93,229,0.15);
|
||
}
|
||
.lh-filter-select option { background: #ffffff; color: #1a1433; }
|
||
/* date inputs inherit filter-select style */
|
||
input[type="date"].lh-filter-select { min-width: 130px; color: var(--lh-text); }
|
||
input[type="date"].lh-filter-select::-webkit-calendar-picker-indicator { opacity: 0.5; cursor: pointer; }
|
||
.lh-filter-label {
|
||
font-size: 0.75rem; font-weight: 700; color: var(--lh-text-3);
|
||
white-space: nowrap; align-self: center;
|
||
}
|
||
/* Admin banner */
|
||
.lh-admin-banner {
|
||
display: inline-flex; align-items: center; gap: 7px;
|
||
background: linear-gradient(135deg, rgba(155,93,229,0.1), rgba(6,214,224,0.06));
|
||
border: 1px solid rgba(155,93,229,0.25); border-radius: 99px;
|
||
padding: 4px 14px; font-size: 0.72rem; font-weight: 700; color: #9B5DE5;
|
||
}
|
||
/* Teacher badge in card */
|
||
.lh-card-teacher {
|
||
font-size: 0.72rem; font-weight: 700; color: #9B5DE5;
|
||
background: rgba(155,93,229,0.1); padding: 3px 10px; border-radius: 99px;
|
||
display: flex; align-items: center; gap: 4px; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||
}
|
||
/* 5th KPI card (admin-only) */
|
||
.lh-kpi-card.admin-extra { display: none; }
|
||
.lh-admin .lh-kpi-card.admin-extra { display: flex; }
|
||
.lh-search {
|
||
display: flex; align-items: center; gap: 8px;
|
||
border: 1.5px solid var(--lh-border); border-radius: 10px;
|
||
padding: 8px 14px; background: var(--lh-surface);
|
||
transition: border-color .18s, box-shadow .18s;
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
.lh-search:focus-within {
|
||
border-color: var(--violet);
|
||
box-shadow: 0 0 0 3px rgba(155,93,229,0.15);
|
||
}
|
||
.lh-search svg { color: var(--lh-text-3); flex-shrink: 0; }
|
||
.lh-search input {
|
||
border: none; outline: none; background: none;
|
||
font-family: 'Manrope', sans-serif; font-size: 0.82rem; color: var(--lh-text);
|
||
width: 190px;
|
||
}
|
||
.lh-search input::placeholder { color: var(--lh-text-3); }
|
||
|
||
/* ── KPI cards ── */
|
||
.lh-kpi {
|
||
display: flex; gap: 8px; padding: 10px 32px 12px; flex-shrink: 0; flex-wrap: wrap;
|
||
}
|
||
.lh-kpi-card {
|
||
flex: 1; min-width: 140px;
|
||
background: var(--lh-surface);
|
||
border: 1px solid var(--lh-border);
|
||
border-radius: 12px; padding: 10px 14px;
|
||
display: flex; align-items: center; gap: 12px;
|
||
transition: border-color .2s, box-shadow .2s;
|
||
position: relative; overflow: hidden;
|
||
}
|
||
.lh-kpi-card::before {
|
||
content: ''; position: absolute; inset: 0;
|
||
background: linear-gradient(135deg, rgba(155,93,229,0.03), rgba(6,214,224,0.01));
|
||
pointer-events: none;
|
||
}
|
||
.lh-kpi-card:hover {
|
||
border-color: rgba(155,93,229,0.25);
|
||
box-shadow: 0 2px 12px rgba(155,93,229,0.1);
|
||
}
|
||
.lh-kpi-icon {
|
||
width: 36px; height: 36px; border-radius: 10px;
|
||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||
}
|
||
.lh-kpi-icon svg { width: 16px; height: 16px; }
|
||
.lh-kpi-icon.violet { background: rgba(155,93,229,0.12); color: #9B5DE5; }
|
||
.lh-kpi-icon.cyan { background: rgba(6,214,224,0.1); color: #06D6E0; }
|
||
.lh-kpi-icon.pink { background: rgba(241,91,181,0.1); color: #F15BB5; }
|
||
.lh-kpi-icon.yellow { background: rgba(255,224,102,0.1); color: #FFE066; }
|
||
.lh-kpi-val {
|
||
font-family: 'Unbounded', sans-serif; font-size: 1.05rem; font-weight: 800;
|
||
color: var(--lh-text);
|
||
}
|
||
@keyframes lh-count-in {
|
||
from { opacity: 0; transform: translateY(6px) scale(0.92); }
|
||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||
}
|
||
.lh-kpi-lbl { font-size: 0.68rem; color: var(--lh-text-3); margin-top: 1px; font-weight: 500; }
|
||
|
||
/* ── Body (list + detail) ── */
|
||
.lh-body {
|
||
display: flex; flex: 1; overflow: hidden; gap: 0;
|
||
}
|
||
|
||
/* ── Session list panel ── */
|
||
.lh-list-panel {
|
||
width: 350px; flex-shrink: 0;
|
||
border-right: 1px solid var(--lh-border);
|
||
display: flex; flex-direction: column;
|
||
overflow: hidden;
|
||
background: var(--lh-surface-2);
|
||
}
|
||
@media (max-width: 768px) {
|
||
.lh-list-panel { width: 100%; border-right: none; }
|
||
.lh-list-panel.hidden-mobile { display: none; }
|
||
.lh-detail-panel.hidden-mobile { display: none; }
|
||
}
|
||
.lh-list-scroll {
|
||
flex: 1; overflow-y: auto; padding: 12px;
|
||
scrollbar-width: thin; scrollbar-color: rgba(155,93,229,0.2) transparent;
|
||
}
|
||
|
||
/* ── Session card ── */
|
||
.lh-session-card {
|
||
position: relative;
|
||
background: var(--lh-surface); border: 1px solid var(--lh-border);
|
||
border-left: 3px solid rgba(155,93,229,0.4);
|
||
border-radius: var(--r); padding: 14px 16px; cursor: pointer;
|
||
transition: all .18s ease; margin-bottom: 8px;
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
.lh-session-card:hover {
|
||
border-color: var(--lh-border-h);
|
||
border-left-color: #9B5DE5;
|
||
box-shadow: 0 8px 32px rgba(155,93,229,0.2);
|
||
transform: translateY(-2px);
|
||
}
|
||
.lh-session-card.active {
|
||
border-color: rgba(155,93,229,0.35);
|
||
border-left-color: #9B5DE5;
|
||
background: linear-gradient(135deg, rgba(155,93,229,0.1), rgba(6,214,224,0.04));
|
||
box-shadow: 0 0 24px rgba(155,93,229,0.12);
|
||
}
|
||
.lh-card-row1 {
|
||
display: flex; align-items: flex-start; justify-content: space-between; gap: 8px;
|
||
margin-bottom: 8px;
|
||
padding-right: 30px; /* clearance for .lh-card-del */
|
||
}
|
||
.lh-card-title {
|
||
font-weight: 700; font-size: 0.85rem; color: var(--lh-text);
|
||
line-height: 1.35; flex: 1;
|
||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||
}
|
||
.lh-card-date {
|
||
font-size: 0.72rem; color: var(--lh-text-3); white-space: nowrap; flex-shrink: 0; margin-top: 2px;
|
||
}
|
||
.lh-card-row2 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||
.lh-card-class {
|
||
font-size: 0.72rem; font-weight: 700; color: #9B5DE5;
|
||
background: rgba(155,93,229,0.12); padding: 3px 10px; border-radius: 99px;
|
||
}
|
||
.lh-card-stat {
|
||
display: flex; align-items: center; gap: 4px;
|
||
font-size: 0.72rem; color: var(--lh-text-2); font-weight: 500;
|
||
}
|
||
.lh-card-stat svg { color: var(--lh-text-3); }
|
||
|
||
/* ── Card delete button ── */
|
||
.lh-card-del {
|
||
position: absolute; top: 10px; right: 10px;
|
||
width: 28px; height: 28px; border-radius: 8px;
|
||
background: rgba(255,80,80,0.08); border: 1px solid rgba(255,80,80,0.15);
|
||
color: #ff6b6b; cursor: pointer;
|
||
display: none; align-items: center; justify-content: center;
|
||
transition: all .18s; z-index: 2; padding: 0;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
.lh-session-card:hover .lh-card-del {
|
||
display: flex;
|
||
animation: lh-fade-in 0.15s ease;
|
||
}
|
||
@keyframes lh-fade-in { from { opacity: 0; transform: scale(0.85); } to { opacity: 1; transform: scale(1); } }
|
||
.lh-card-del:hover {
|
||
background: rgba(255,80,80,0.2); border-color: rgba(255,80,80,0.4);
|
||
box-shadow: 0 0 16px rgba(255,80,80,0.2);
|
||
}
|
||
|
||
/* ── Date group separator ── */
|
||
.lh-date-sep {
|
||
font-size: 0.68rem; font-weight: 700; color: var(--lh-text-3);
|
||
text-transform: uppercase; letter-spacing: 0.08em;
|
||
padding: 10px 8px 6px; margin-top: 6px;
|
||
display: flex; align-items: center; gap: 8px;
|
||
}
|
||
.lh-date-sep::before {
|
||
content: '';
|
||
width: 16px; height: 16px; border-radius: 6px;
|
||
background: rgba(155,93,229,0.1);
|
||
flex-shrink: 0;
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
/* calendar icon via background */
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%239B5DE5' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
|
||
background-repeat: no-repeat; background-position: center; background-size: 10px;
|
||
}
|
||
|
||
/* ── Empty states ── */
|
||
.lh-empty {
|
||
text-align: center; padding: 56px 24px; color: var(--lh-text-3);
|
||
display: flex; flex-direction: column; align-items: center; gap: 12px;
|
||
}
|
||
.lh-empty svg {
|
||
display: block; opacity: 0.2;
|
||
filter: drop-shadow(0 0 20px rgba(155,93,229,0.3));
|
||
}
|
||
.lh-empty p { font-size: 0.85rem; margin: 0; }
|
||
|
||
/* ── Pagination ── */
|
||
.lh-pagination {
|
||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||
padding: 12px; border-top: 1px solid var(--lh-border); flex-shrink: 0;
|
||
}
|
||
.lh-page-btn {
|
||
padding: 6px 14px; border-radius: 10px; border: 1px solid var(--lh-border);
|
||
background: var(--lh-surface); color: var(--lh-text-2); font-size: 0.78rem; font-weight: 600;
|
||
cursor: pointer; transition: all .15s;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
.lh-page-btn:hover:not(:disabled) {
|
||
border-color: var(--violet); color: #9B5DE5;
|
||
box-shadow: 0 0 12px rgba(155,93,229,0.15);
|
||
}
|
||
.lh-page-btn:disabled { opacity: 0.35; cursor: default; }
|
||
.lh-page-info { font-size: 0.78rem; color: var(--lh-text-3); }
|
||
|
||
/* ── Sort row ── */
|
||
.lh-list-header { padding: 10px 12px 0; flex-shrink: 0; }
|
||
.lh-sort-row {
|
||
display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0 4px 8px;
|
||
}
|
||
.lh-count-badge { font-size: 0.75rem; color: var(--lh-text-3); font-weight: 600; }
|
||
.lh-sort-select {
|
||
padding: 5px 10px; border-radius: 8px; border: 1px solid var(--lh-border);
|
||
background: var(--lh-surface); color: var(--lh-text-2); font-family: 'Manrope', sans-serif;
|
||
font-size: 0.75rem; font-weight: 600; cursor: pointer; outline: none;
|
||
transition: border-color .15s;
|
||
}
|
||
.lh-sort-select:focus { border-color: var(--violet); }
|
||
.lh-sort-select option { background: #ffffff; color: #1a1433; }
|
||
|
||
/* ── Detail panel ── */
|
||
.lh-detail-panel {
|
||
flex: 1; display: flex; flex-direction: column; overflow: hidden;
|
||
background: var(--lh-bg);
|
||
}
|
||
.lh-detail-empty {
|
||
flex: 1; display: flex; flex-direction: column;
|
||
align-items: center; justify-content: center; gap: 18px;
|
||
color: var(--lh-text-3); padding: 40px;
|
||
}
|
||
.lh-detail-empty svg {
|
||
opacity: 0.15;
|
||
filter: drop-shadow(0 0 30px rgba(155,93,229,0.3));
|
||
}
|
||
.lh-detail-empty p { font-size: 0.9rem; margin: 0; }
|
||
.lh-detail-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||
|
||
/* ── Detail header ── */
|
||
.lh-detail-hdr {
|
||
padding: 10px 22px 8px;
|
||
border-bottom: 1px solid var(--lh-border);
|
||
background: var(--lh-surface);
|
||
flex-shrink: 0;
|
||
}
|
||
.lh-detail-back {
|
||
display: none; align-items: center; gap: 6px;
|
||
font-size: 0.78rem; font-weight: 600; color: var(--lh-text-3);
|
||
cursor: pointer; margin-bottom: 12px;
|
||
background: none; border: none; padding: 0;
|
||
transition: color .15s;
|
||
}
|
||
.lh-detail-back:hover { color: #9B5DE5; }
|
||
@media (max-width: 768px) { .lh-detail-back { display: flex; } }
|
||
|
||
.lh-detail-hdr-row {
|
||
display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 6px;
|
||
}
|
||
.lh-detail-title {
|
||
font-family: 'Unbounded', sans-serif; font-size: 0.95rem;
|
||
font-weight: 800; color: var(--lh-text); margin: 0;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1;
|
||
}
|
||
.lh-detail-meta {
|
||
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
|
||
}
|
||
.lh-meta-item {
|
||
display: inline-flex; align-items: center; gap: 5px;
|
||
font-size: 0.7rem; font-weight: 600;
|
||
color: var(--lh-text-2);
|
||
background: rgba(155,93,229,0.06);
|
||
padding: 3px 9px; border-radius: 99px;
|
||
border: 1px solid rgba(155,93,229,0.1);
|
||
}
|
||
.lh-meta-item svg { color: var(--lh-text-3); }
|
||
|
||
/* ── Detail stats chips ── */
|
||
.lh-detail-stats {
|
||
display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px;
|
||
}
|
||
.lh-stat-chip {
|
||
display: flex; align-items: center; gap: 5px;
|
||
background: var(--lh-surface);
|
||
border: 1px solid var(--lh-border);
|
||
border-radius: 99px;
|
||
padding: 3px 10px; font-size: 0.7rem; font-weight: 700; color: var(--lh-text-2);
|
||
transition: border-color .15s;
|
||
}
|
||
.lh-stat-chip:hover { border-color: rgba(155,93,229,0.25); }
|
||
.lh-stat-chip svg { color: var(--lh-text-3); }
|
||
|
||
/* ── Detail delete button ── */
|
||
.lh-det-del-btn {
|
||
display: none; flex-shrink: 0; align-items: center; gap: 5px;
|
||
padding: 5px 11px; border-radius: 8px;
|
||
background: rgba(255,80,80,0.06); border: 1px solid rgba(255,80,80,0.18);
|
||
color: #ff6b6b; font-size: 0.74rem; font-weight: 700; font-family: 'Manrope', sans-serif;
|
||
cursor: pointer; transition: all .18s; white-space: nowrap;
|
||
}
|
||
.lh-det-del-btn.visible { display: flex; }
|
||
.lh-det-del-btn:hover {
|
||
background: rgba(255,80,80,0.14);
|
||
border-color: rgba(255,80,80,0.35);
|
||
box-shadow: 0 0 20px rgba(255,80,80,0.15);
|
||
}
|
||
|
||
/* ── Tabs (pill style) ── */
|
||
.lh-tabs {
|
||
display: flex; gap: 4px; padding: 8px 22px;
|
||
border-bottom: 1px solid var(--lh-border);
|
||
background: var(--lh-surface);
|
||
flex-shrink: 0;
|
||
}
|
||
.lh-tab {
|
||
padding: 6px 14px; border-radius: 8px;
|
||
border: 1px solid transparent;
|
||
font-size: 0.79rem; font-weight: 600; color: var(--lh-text-3);
|
||
cursor: pointer; background: none; transition: all .18s;
|
||
display: flex; align-items: center; gap: 6px;
|
||
}
|
||
.lh-tab:hover {
|
||
color: var(--lh-text-2);
|
||
background: rgba(155,93,229,0.07);
|
||
}
|
||
.lh-tab.active {
|
||
color: #9B5DE5;
|
||
background: rgba(155,93,229,0.1);
|
||
border-color: rgba(155,93,229,0.25);
|
||
box-shadow: 0 0 16px rgba(155,93,229,0.08);
|
||
}
|
||
.lh-tab svg { width: 13px; height: 13px; }
|
||
.lh-tab-content { flex: 1; overflow: hidden; min-height: 0; }
|
||
|
||
/* ── Board tab ── */
|
||
.lh-board-wrap {
|
||
height: 100%; display: flex; flex-direction: column; overflow: hidden;
|
||
padding: 0 20px 40px 0;
|
||
}
|
||
.lh-board-main {
|
||
flex: 1; display: flex; flex-direction: column; overflow: hidden; min-height: 0;
|
||
position: relative;
|
||
}
|
||
|
||
/* Topbar */
|
||
.lh-board-topbar {
|
||
height: 48px; flex-shrink: 0; position: relative;
|
||
background: #0e0b1e;
|
||
border-bottom: 1px solid rgba(155,93,229,0.18);
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 0 16px; gap: 8px;
|
||
}
|
||
.lh-board-nav-btn {
|
||
width: 32px; height: 32px; border-radius: 9px;
|
||
background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.09);
|
||
color: rgba(255,255,255,0.55); cursor: pointer;
|
||
display: flex; align-items: center; justify-content: center;
|
||
transition: all .15s; flex-shrink: 0;
|
||
}
|
||
.lh-board-nav-btn:hover:not(:disabled) {
|
||
background: rgba(155,93,229,0.25);
|
||
border-color: rgba(155,93,229,0.4);
|
||
color: #c4a0f7;
|
||
}
|
||
.lh-board-nav-btn:disabled { opacity: 0.18; cursor: default; }
|
||
|
||
/* Page indicator button */
|
||
.lh-page-indicator {
|
||
display: flex; align-items: center; gap: 8px;
|
||
padding: 6px 14px; border-radius: 10px;
|
||
background: rgba(155,93,229,0.06);
|
||
border: 1.5px solid rgba(155,93,229,0.18);
|
||
color: rgba(255,255,255,0.85); cursor: pointer;
|
||
font-family: 'Manrope', sans-serif; font-size: 12px; font-weight: 700;
|
||
transition: all .15s;
|
||
white-space: nowrap;
|
||
position: relative;
|
||
}
|
||
.lh-page-indicator::before {
|
||
content: ''; position: absolute; inset: -1px; border-radius: 11px;
|
||
background: linear-gradient(135deg, rgba(155,93,229,0.2), rgba(6,214,224,0.1));
|
||
z-index: -1; opacity: 0;
|
||
transition: opacity .15s;
|
||
}
|
||
.lh-page-indicator:hover {
|
||
background: rgba(155,93,229,0.12);
|
||
border-color: rgba(155,93,229,0.35);
|
||
}
|
||
.lh-page-indicator:hover::before { opacity: 1; }
|
||
.lh-page-indicator.open {
|
||
background: rgba(155,93,229,0.15);
|
||
border-color: #9B5DE5;
|
||
box-shadow: 0 0 16px rgba(155,93,229,0.2);
|
||
}
|
||
.lh-page-ind-title { max-width: 160px; overflow: hidden; text-overflow: ellipsis; }
|
||
.lh-page-ind-counter { color: rgba(155,93,229,0.75); font-size: 11px; }
|
||
.lh-page-ind-chevron { color: rgba(255,255,255,0.35); transition: transform .18s; }
|
||
.lh-page-indicator.open .lh-page-ind-chevron { transform: rotate(180deg); }
|
||
|
||
/* Pages popup */
|
||
.lh-pages-popup {
|
||
position: absolute; top: calc(100% + 8px); left: 50%; transform: translateX(-50%);
|
||
z-index: 200;
|
||
background: rgba(18,14,32,0.92); border: 1px solid rgba(155,93,229,0.25);
|
||
border-radius: 16px; padding: 14px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.7), 0 0 40px rgba(155,93,229,0.08);
|
||
backdrop-filter: blur(20px);
|
||
display: grid; grid-template-columns: repeat(4, 120px); gap: 10px;
|
||
max-height: 70vh; overflow-y: auto;
|
||
scrollbar-width: thin; scrollbar-color: rgba(155,93,229,0.3) transparent;
|
||
}
|
||
.lh-pages-popup.hidden { display: none; }
|
||
@media (max-width: 700px) { .lh-pages-popup { grid-template-columns: repeat(2, 140px); } }
|
||
|
||
/* Thumbnail card */
|
||
.lh-thumb-item {
|
||
cursor: pointer; border-radius: 10px;
|
||
border: 2px solid rgba(255,255,255,0.06);
|
||
background: rgba(28,22,48,0.8); overflow: hidden;
|
||
transition: all .18s;
|
||
}
|
||
.lh-thumb-item:hover {
|
||
border-color: rgba(155,93,229,0.45);
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.4), 0 0 16px rgba(155,93,229,0.12);
|
||
transform: translateY(-2px);
|
||
}
|
||
.lh-thumb-item.active {
|
||
border-color: #9B5DE5;
|
||
box-shadow: 0 0 0 3px rgba(155,93,229,0.2), 0 4px 20px rgba(0,0,0,0.3);
|
||
}
|
||
.lh-thumb-canvas-wrap { width: 100%; aspect-ratio: 16/9; background: #2d5a2d; overflow: hidden; }
|
||
.lh-thumb-canvas-wrap canvas { display: block; width: 100%; height: 100%; }
|
||
.lh-thumb-footer {
|
||
padding: 6px 8px;
|
||
display: flex; align-items: center; justify-content: space-between; gap: 4px;
|
||
}
|
||
.lh-thumb-lbl {
|
||
font-size: 10px; color: rgba(255,255,255,0.55); font-family: 'Manrope', sans-serif;
|
||
font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1;
|
||
}
|
||
.lh-thumb-num {
|
||
font-size: 10px; color: rgba(155,93,229,0.65); font-family: 'Manrope', sans-serif; font-weight: 800;
|
||
}
|
||
|
||
/* Canvas */
|
||
.lh-board-canvas-wrap {
|
||
flex: 1; position: relative; overflow: hidden;
|
||
background: #2d5a2d; min-height: 0;
|
||
}
|
||
.lh-board-canvas-wrap::after {
|
||
content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 5;
|
||
background:
|
||
linear-gradient(to bottom, rgba(0,0,0,0.18) 0%, transparent 60px),
|
||
linear-gradient(to top, rgba(0,0,0,0.12) 0%, transparent 80px);
|
||
}
|
||
.lh-board-canvas { position: absolute; top: 0; left: 0; display: block; }
|
||
|
||
/* Zoom bar */
|
||
.lh-board-zoom {
|
||
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
|
||
display: flex; align-items: center; gap: 1px; z-index: 20;
|
||
background: rgba(10,7,24,0.88);
|
||
border: 1px solid rgba(155,93,229,0.22);
|
||
border-radius: 14px; padding: 5px 6px;
|
||
backdrop-filter: blur(16px);
|
||
box-shadow: 0 4px 24px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04) inset;
|
||
}
|
||
.lh-zoom-btn {
|
||
width: 34px; height: 34px; border-radius: 9px; background: transparent; border: none;
|
||
color: rgba(255,255,255,0.55); cursor: pointer;
|
||
display: flex; align-items: center; justify-content: center;
|
||
transition: background .15s, color .15s;
|
||
}
|
||
.lh-zoom-btn:hover {
|
||
background: rgba(155,93,229,0.25);
|
||
color: #c4a0f7;
|
||
}
|
||
.lh-zoom-lbl {
|
||
font-size: 12px; color: rgba(255,255,255,0.7); font-family: 'Manrope', sans-serif;
|
||
font-weight: 700; min-width: 44px; text-align: center;
|
||
}
|
||
.lh-zoom-divider { width: 1px; height: 18px; background: rgba(255,255,255,0.08); margin: 0 4px; }
|
||
|
||
/* Export */
|
||
.lh-board-export { position: absolute; top: 14px; right: 16px; z-index: 20; }
|
||
.lh-export-btn {
|
||
display: flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 10px;
|
||
background: rgba(10,7,24,0.75);
|
||
border: 1px solid rgba(155,93,229,0.22);
|
||
color: rgba(255,255,255,0.8); font-size: 11px; font-family: 'Manrope', sans-serif;
|
||
font-weight: 700; cursor: pointer; transition: all .18s;
|
||
backdrop-filter: blur(12px);
|
||
box-shadow: 0 2px 12px rgba(0,0,0,0.35);
|
||
}
|
||
.lh-export-btn:hover {
|
||
background: rgba(155,93,229,0.3);
|
||
color: #fff; border-color: rgba(155,93,229,0.45);
|
||
box-shadow: 0 4px 20px rgba(155,93,229,0.2);
|
||
}
|
||
|
||
/* Loading */
|
||
.lh-board-loading {
|
||
position: absolute; inset: 0; display: flex; flex-direction: column;
|
||
align-items: center; justify-content: center; gap: 14px;
|
||
background: #2d5a2d; color: rgba(255,255,255,0.45);
|
||
font-family: 'Manrope', sans-serif; font-size: 0.82rem; z-index: 30;
|
||
}
|
||
.lh-board-spinner {
|
||
width: 36px; height: 36px; border-radius: 50%;
|
||
border: 3px solid rgba(155,93,229,0.12); border-top-color: #9B5DE5;
|
||
animation: lh-spin 0.75s linear infinite;
|
||
}
|
||
@keyframes lh-spin { to { transform: rotate(360deg); } }
|
||
|
||
/* ── Chat tab ── */
|
||
.lh-chat-wrap {
|
||
height: 100%; overflow-y: auto; padding: 18px 22px;
|
||
display: flex; flex-direction: column; gap: 6px;
|
||
scrollbar-width: thin; scrollbar-color: rgba(155,93,229,0.2) transparent;
|
||
}
|
||
.lh-chat-toolbar {
|
||
display: flex; justify-content: flex-end; padding: 12px 22px 0;
|
||
flex-shrink: 0;
|
||
}
|
||
.lh-chat-export-btn {
|
||
display: flex; align-items: center; gap: 7px;
|
||
padding: 7px 16px; border-radius: 10px;
|
||
background: rgba(155,93,229,0.08); border: 1px solid rgba(155,93,229,0.2);
|
||
color: #9B5DE5; font-size: 0.78rem; font-weight: 700; cursor: pointer;
|
||
text-decoration: none; transition: all .15s;
|
||
}
|
||
.lh-chat-export-btn:hover {
|
||
background: rgba(155,93,229,0.15);
|
||
box-shadow: 0 0 12px rgba(155,93,229,0.1);
|
||
}
|
||
|
||
/* Message bubble */
|
||
.lh-msg {
|
||
display: flex; align-items: flex-start; gap: 12px;
|
||
padding: 10px 14px; border-radius: 12px;
|
||
transition: background .15s;
|
||
}
|
||
.lh-msg:hover { background: rgba(155,93,229,0.04); }
|
||
.lh-msg-avatar {
|
||
width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 13px; font-weight: 700; color: #fff;
|
||
box-shadow: 0 0 0 2px rgba(155,93,229,0.2), 0 0 0 4px rgba(155,93,229,0.05);
|
||
}
|
||
.lh-msg-body { flex: 1; min-width: 0; }
|
||
.lh-msg-head {
|
||
display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px;
|
||
}
|
||
.lh-msg-name { font-size: 0.8rem; font-weight: 700; color: var(--lh-text); }
|
||
.lh-msg-time { font-size: 0.72rem; color: var(--lh-text-3); }
|
||
.lh-msg-text {
|
||
font-size: 0.83rem; color: var(--lh-text-2); line-height: 1.55;
|
||
word-break: break-word; white-space: pre-wrap;
|
||
background: rgba(155,93,229,0.04);
|
||
padding: 8px 14px; border-radius: 0 12px 12px 12px;
|
||
border: 1px solid rgba(155,93,229,0.06);
|
||
}
|
||
.lh-msg-img { max-width: 220px; border-radius: 10px; margin-top: 6px; display: block; }
|
||
|
||
/* Pinned message */
|
||
.lh-msg.pinned {
|
||
background: rgba(255,224,102,0.04);
|
||
border: 1px solid rgba(255,224,102,0.12);
|
||
border-radius: 12px; padding: 10px 14px;
|
||
}
|
||
.lh-msg.pinned .lh-msg-name {
|
||
color: #FFE066;
|
||
}
|
||
.lh-msg.pinned .lh-msg-name::after {
|
||
content: '';
|
||
display: inline-block; width: 12px; height: 12px; margin-left: 6px;
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23FFE066' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='12' y1='17' x2='12' y2='22'/%3E%3Cpath d='M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z'/%3E%3C/svg%3E");
|
||
background-repeat: no-repeat; background-size: contain;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
/* ── Attendance tab ── */
|
||
.lh-attend-wrap {
|
||
height: 100%; overflow-y: auto; padding: 18px 22px;
|
||
scrollbar-width: thin; scrollbar-color: rgba(155,93,229,0.2) transparent;
|
||
}
|
||
.lh-attend-summary {
|
||
display: flex; gap: 12px; margin-bottom: 18px; flex-wrap: wrap;
|
||
}
|
||
.lh-attend-kpi {
|
||
background: var(--lh-surface);
|
||
border: 1px solid var(--lh-border);
|
||
border-radius: var(--r); padding: 12px 18px;
|
||
text-align: center; min-width: 110px;
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
.lh-attend-kpi-val {
|
||
font-family: 'Unbounded', sans-serif; font-size: 1.1rem; font-weight: 800; color: var(--lh-text);
|
||
}
|
||
.lh-attend-kpi-lbl { font-size: 0.72rem; color: var(--lh-text-3); margin-top: 3px; }
|
||
|
||
/* Attendance table */
|
||
.lh-attend-table { width: 100%; border-collapse: collapse; }
|
||
.lh-attend-table th, .lh-attend-table td {
|
||
text-align: left; padding: 11px 14px;
|
||
border-bottom: 1px solid var(--lh-border);
|
||
font-size: 0.82rem;
|
||
}
|
||
.lh-attend-table th {
|
||
font-weight: 700; color: var(--lh-text-3); font-size: 0.73rem;
|
||
text-transform: uppercase; letter-spacing: 0.05em;
|
||
background: rgba(155,93,229,0.04);
|
||
position: sticky; top: 0; z-index: 2;
|
||
backdrop-filter: blur(6px);
|
||
}
|
||
.lh-attend-table td { color: var(--lh-text-2); }
|
||
.lh-attend-table tbody tr:nth-child(even) { background: rgba(155,93,229,0.02); }
|
||
.lh-attend-table tbody tr:hover { background: rgba(155,93,229,0.06); }
|
||
.lh-attend-table td strong { color: var(--lh-text); }
|
||
.lh-attend-dur {
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
background: linear-gradient(135deg, rgba(155,93,229,0.1), rgba(6,214,224,0.06));
|
||
border: 1px solid rgba(155,93,229,0.15);
|
||
border-radius: 99px;
|
||
padding: 3px 11px; font-size: 0.75rem; font-weight: 700; color: #9B5DE5;
|
||
}
|
||
|
||
/* ── Notes tab ── */
|
||
.lh-notes-wrap {
|
||
height: 100%; overflow-y: auto; padding: 18px 22px;
|
||
scrollbar-width: thin; scrollbar-color: rgba(155,93,229,0.2) transparent;
|
||
}
|
||
.lh-note-card {
|
||
background: var(--lh-surface); border: 1px solid var(--lh-border);
|
||
border-radius: var(--r); padding: 16px 18px; margin-bottom: 12px;
|
||
backdrop-filter: blur(8px);
|
||
transition: border-color .15s, box-shadow .15s;
|
||
}
|
||
.lh-note-card:hover {
|
||
border-color: rgba(155,93,229,0.2);
|
||
box-shadow: 0 4px 16px rgba(155,93,229,0.08);
|
||
}
|
||
.lh-note-head {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
margin-bottom: 10px;
|
||
}
|
||
.lh-note-author {
|
||
font-weight: 700; font-size: 0.82rem; color: var(--lh-text);
|
||
display: flex; align-items: center; gap: 7px;
|
||
}
|
||
.lh-note-author svg { color: var(--lh-text-3); }
|
||
.lh-note-time { font-size: 0.72rem; color: var(--lh-text-3); }
|
||
.lh-note-text {
|
||
font-size: 0.83rem; color: var(--lh-text-2); line-height: 1.65;
|
||
white-space: pre-wrap; word-break: break-word;
|
||
}
|
||
.lh-own-note-box {
|
||
background: rgba(155,93,229,0.04); border: 1px solid rgba(155,93,229,0.15);
|
||
border-radius: var(--r); padding: 18px; margin-bottom: 12px;
|
||
}
|
||
.lh-own-note-lbl {
|
||
font-size: 0.78rem; font-weight: 700; color: #9B5DE5; margin-bottom: 10px;
|
||
}
|
||
|
||
/* ── Skeleton ── */
|
||
.lh-skeleton {
|
||
background: linear-gradient(90deg, rgba(155,93,229,0.06) 25%, rgba(155,93,229,0.12) 50%, rgba(155,93,229,0.06) 75%);
|
||
background-size: 200% 100%;
|
||
border-radius: 8px;
|
||
animation: lh-shimmer 1.6s ease-in-out infinite;
|
||
}
|
||
@keyframes lh-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||
.lh-sk-card { height: 80px; border-radius: var(--r); margin-bottom: 8px; }
|
||
|
||
/* ── Confirmation modal ── */
|
||
.lh-confirm-overlay {
|
||
position: fixed; inset: 0; background: rgba(6,4,14,0.8);
|
||
display: flex; align-items: center; justify-content: center;
|
||
z-index: 2000; backdrop-filter: blur(8px);
|
||
}
|
||
.lh-confirm-overlay.hidden { display: none; }
|
||
.lh-confirm-dialog {
|
||
background: #ffffff;
|
||
border: 1.5px solid rgba(255,80,80,0.2);
|
||
border-radius: 20px; padding: 32px 32px 26px; max-width: 420px; width: 92%;
|
||
box-shadow: 0 24px 80px rgba(0,0,0,0.25), 0 0 40px rgba(255,80,80,0.04);
|
||
position: relative; overflow: hidden;
|
||
}
|
||
.lh-confirm-dialog::before {
|
||
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
|
||
background: linear-gradient(90deg, transparent, rgba(255,80,80,0.5), transparent);
|
||
}
|
||
.lh-confirm-icon {
|
||
width: 52px; height: 52px; border-radius: 16px;
|
||
background: rgba(255,80,80,0.08); border: 1.5px solid rgba(255,80,80,0.2);
|
||
display: flex; align-items: center; justify-content: center;
|
||
color: #ff6b6b; margin-bottom: 18px;
|
||
animation: lh-pulse-icon 2s ease-in-out infinite;
|
||
}
|
||
@keyframes lh-pulse-icon {
|
||
0%, 100% { box-shadow: 0 0 0 0 rgba(255,80,80,0.2); }
|
||
50% { box-shadow: 0 0 0 8px rgba(255,80,80,0); }
|
||
}
|
||
.lh-confirm-cancel {
|
||
padding: 9px 20px; border-radius: 10px; border: 1px solid rgba(155,93,229,0.18);
|
||
background: transparent; color: #4a3f6b; font-family: 'Manrope', sans-serif;
|
||
font-size: 0.82rem; font-weight: 700; cursor: pointer; transition: all .15s;
|
||
}
|
||
.lh-confirm-cancel:hover { border-color: #8f86a8; color: #1a1433; }
|
||
.lh-confirm-title {
|
||
font-family: 'Unbounded', sans-serif; font-size: 0.95rem; font-weight: 800;
|
||
color: #1a1433; margin: 0 0 10px;
|
||
}
|
||
.lh-confirm-desc {
|
||
font-size: 0.83rem; color: #4a3f6b; line-height: 1.6; margin: 0 0 24px;
|
||
}
|
||
.lh-confirm-sname { font-weight: 700; color: #1a1433; }
|
||
.lh-confirm-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||
.lh-confirm-delete {
|
||
padding: 9px 20px; border-radius: 10px; border: 1.5px solid rgba(255,80,80,0.3);
|
||
background: rgba(255,80,80,0.08); color: #ff6b6b;
|
||
font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 700;
|
||
cursor: pointer; transition: all .15s;
|
||
}
|
||
.lh-confirm-delete:hover {
|
||
background: rgba(255,80,80,0.18);
|
||
box-shadow: 0 0 20px rgba(255,80,80,0.15);
|
||
}
|
||
.lh-confirm-delete:disabled { opacity: 0.4; cursor: default; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-layout">
|
||
<aside class="sidebar" id="app-sidebar"></aside>
|
||
|
||
<main class="sb-content">
|
||
<div class="lh-wrap">
|
||
|
||
<!-- Header + filters -->
|
||
<div class="lh-header">
|
||
<div class="lh-header-top">
|
||
<h1 class="lh-title">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>
|
||
Архив уроков
|
||
</h1>
|
||
</div>
|
||
<div class="lh-filters">
|
||
<!-- Teacher filter: admin only -->
|
||
<select class="lh-filter-select" id="lh-teacher-filter" onchange="onTeacherFilter()" style="display:none">
|
||
<option value="">Все учителя</option>
|
||
</select>
|
||
<!-- Class filter: teacher only (admin uses search) -->
|
||
<select class="lh-filter-select" id="lh-class-filter" onchange="onClassFilter()">
|
||
<option value="">Все классы</option>
|
||
</select>
|
||
<!-- Date range: admin only -->
|
||
<span class="lh-filter-label" id="lh-date-label" style="display:none">с</span>
|
||
<input type="date" class="lh-filter-select" id="lh-date-from" onchange="onDateFilter()" style="display:none" title="С даты"/>
|
||
<span class="lh-filter-label" id="lh-date-label2" style="display:none">по</span>
|
||
<input type="date" class="lh-filter-select" id="lh-date-to" onchange="onDateFilter()" style="display:none" title="По дату"/>
|
||
<div class="lh-search">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||
<input type="text" id="lh-search" placeholder="Поиск по названию..." oninput="onSearch()"/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- KPI row -->
|
||
<div class="lh-kpi" id="lh-kpi">
|
||
<div class="lh-kpi-card">
|
||
<div class="lh-kpi-icon violet"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg></div>
|
||
<div><div class="lh-kpi-val" id="kpi-total">—</div><div class="lh-kpi-lbl" id="kpi-total-lbl">Уроков проведено</div></div>
|
||
</div>
|
||
<div class="lh-kpi-card">
|
||
<div class="lh-kpi-icon cyan"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
|
||
<div><div class="lh-kpi-val" id="kpi-time">—</div><div class="lh-kpi-lbl">Общее время</div></div>
|
||
</div>
|
||
<!-- Admin-only: teachers count -->
|
||
<div class="lh-kpi-card admin-extra">
|
||
<div class="lh-kpi-icon violet" style="background:rgba(255,159,67,0.1);color:#FF9F43"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div>
|
||
<div><div class="lh-kpi-val" id="kpi-teachers">—</div><div class="lh-kpi-lbl">Учителей</div></div>
|
||
</div>
|
||
<div class="lh-kpi-card">
|
||
<div class="lh-kpi-icon pink"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></div>
|
||
<div><div class="lh-kpi-val" id="kpi-students">—</div><div class="lh-kpi-lbl">Учеников охвачено</div></div>
|
||
</div>
|
||
<div class="lh-kpi-card">
|
||
<div class="lh-kpi-icon yellow"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></div>
|
||
<div><div class="lh-kpi-val" id="kpi-msgs">—</div><div class="lh-kpi-lbl">Сообщений в чатах</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Body: list + detail -->
|
||
<div class="lh-body">
|
||
|
||
<!-- Session list -->
|
||
<div class="lh-list-panel" id="lh-list-panel">
|
||
<div class="lh-list-header">
|
||
<div class="lh-sort-row">
|
||
<span class="lh-count-badge" id="lh-count-badge"></span>
|
||
<select class="lh-sort-select" id="lh-sort" onchange="onSort()">
|
||
<option value="newest">Сначала новые</option>
|
||
<option value="oldest">Сначала старые</option>
|
||
<option value="longest">По длительности</option>
|
||
<option value="most_students" class="admin-opt" style="display:none">По числу учеников</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="lh-list-scroll" id="lh-list">
|
||
<div class="lh-sk-card lh-skeleton"></div>
|
||
<div class="lh-sk-card lh-skeleton"></div>
|
||
<div class="lh-sk-card lh-skeleton"></div>
|
||
</div>
|
||
<div class="lh-pagination" id="lh-pagination" style="display:none">
|
||
<button class="lh-page-btn" id="lh-prev" onclick="changePage(-1)">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||
</button>
|
||
<span class="lh-page-info" id="lh-page-info">1 / 1</span>
|
||
<button class="lh-page-btn" id="lh-next" onclick="changePage(1)">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Detail view -->
|
||
<div class="lh-detail-panel" id="lh-detail-panel">
|
||
<div class="lh-detail-empty" id="lh-detail-empty">
|
||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>
|
||
<p>Выберите урок из списка слева</p>
|
||
</div>
|
||
<div class="lh-detail-content" id="lh-detail-content" style="display:none">
|
||
<div class="lh-detail-hdr">
|
||
<button class="lh-detail-back" onclick="backToList()">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||
Назад к списку
|
||
</button>
|
||
<div class="lh-detail-hdr-row">
|
||
<h2 class="lh-detail-title" id="det-title">—</h2>
|
||
<button class="lh-det-del-btn" id="det-del-btn" onclick="confirmDelete(null)">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
||
Удалить
|
||
</button>
|
||
</div>
|
||
<div class="lh-detail-meta">
|
||
<span class="lh-meta-item" id="det-date">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
||
</span>
|
||
<span class="lh-meta-item" id="det-teacher">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||
</span>
|
||
<span class="lh-meta-item" id="det-class" style="display:none">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 10v6M2 10l10-5 10 5-10 5z"/><path d="M6 12v5c3 3 9 3 12 0v-5"/></svg>
|
||
</span>
|
||
</div>
|
||
<div class="lh-detail-stats" id="det-stats"></div>
|
||
</div>
|
||
|
||
<!-- Tabs -->
|
||
<div class="lh-tabs">
|
||
<button class="lh-tab active" id="tab-board" onclick="switchTab('board')">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
|
||
Доска
|
||
</button>
|
||
<button class="lh-tab" id="tab-chat" onclick="switchTab('chat')">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||
Чат
|
||
<span id="tab-chat-count" style="display:none;background:rgba(155,93,229,.15);color:#9B5DE5;font-size:10px;padding:2px 7px;border-radius:99px;font-weight:700"></span>
|
||
</button>
|
||
<button class="lh-tab" id="tab-attend" onclick="switchTab('attend')">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||
Посещаемость
|
||
</button>
|
||
<button class="lh-tab" id="tab-notes" onclick="switchTab('notes')">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||
Заметки
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Tab contents -->
|
||
<div class="lh-tab-content">
|
||
<!-- Board -->
|
||
<div id="tc-board" class="lh-board-wrap">
|
||
<div class="lh-board-main">
|
||
<!-- Topbar: prev / [page indicator] / next -->
|
||
<div class="lh-board-topbar">
|
||
<button class="lh-board-nav-btn" id="lh-page-prev" onclick="lhPageNav(-1)" title="Предыдущая (←)">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||
</button>
|
||
|
||
<!-- Page indicator -->
|
||
<button class="lh-page-indicator" id="lh-page-indicator" onclick="lhTogglePages()">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
|
||
<span class="lh-page-ind-title" id="lh-page-title">Страница 1</span>
|
||
<span class="lh-page-ind-counter" id="lh-page-total"></span>
|
||
<svg class="lh-page-ind-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||
</button>
|
||
|
||
<!-- Pages popup (thumbnails grid) -->
|
||
<div class="lh-pages-popup hidden" id="lh-pages-popup">
|
||
<div id="lh-thumbs"></div>
|
||
</div>
|
||
|
||
<button class="lh-board-nav-btn" id="lh-page-next" onclick="lhPageNav(1)" title="Следующая (→)">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Canvas -->
|
||
<div class="lh-board-canvas-wrap">
|
||
<canvas id="lh-canvas" class="lh-board-canvas"></canvas>
|
||
<div class="lh-board-loading" id="lh-board-loading">
|
||
<div class="lh-board-spinner"></div>
|
||
<span>Загрузка доски...</span>
|
||
</div>
|
||
<div class="lh-board-export">
|
||
<button class="lh-export-btn" onclick="exportBoardPage()">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||
PNG
|
||
</button>
|
||
</div>
|
||
<div class="lh-board-zoom">
|
||
<button class="lh-zoom-btn" onclick="lhZoom(-0.25)" title="-">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||
</button>
|
||
<span class="lh-zoom-lbl" id="lh-zoom-lbl">100%</span>
|
||
<button class="lh-zoom-btn" onclick="lhZoom(0.25)" title="+">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||
</button>
|
||
<div class="lh-zoom-divider"></div>
|
||
<button class="lh-zoom-btn" onclick="lhZoomFit()" title="Вписать (0)">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chat -->
|
||
<div id="tc-chat" style="display:none;height:100%;flex-direction:column;overflow:hidden">
|
||
<div class="lh-chat-toolbar" id="lh-chat-toolbar"></div>
|
||
<div class="lh-chat-wrap" id="lh-chat-list"></div>
|
||
</div>
|
||
|
||
<!-- Attendance -->
|
||
<div id="tc-attend" style="display:none">
|
||
<div class="lh-attend-wrap">
|
||
<div class="lh-attend-summary" id="lh-attend-summary"></div>
|
||
<table class="lh-attend-table">
|
||
<thead><tr>
|
||
<th>Участник</th><th>Вошёл</th><th>Вышел</th><th>Время</th>
|
||
</tr></thead>
|
||
<tbody id="lh-attend-body"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Notes -->
|
||
<div id="tc-notes" style="display:none">
|
||
<div class="lh-notes-wrap" id="lh-notes-list"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Confirmation modal -->
|
||
<div class="lh-confirm-overlay hidden" id="lh-confirm-overlay">
|
||
<div class="lh-confirm-dialog">
|
||
<div class="lh-confirm-icon">
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
||
</div>
|
||
<h3 class="lh-confirm-title">Удалить запись урока?</h3>
|
||
<p class="lh-confirm-desc">Урок <span class="lh-confirm-sname" id="confirm-sname"></span> будет безвозвратно удалён вместе с доской, чатом, заметками и данными посещаемости. Это действие нельзя отменить.</p>
|
||
<div class="lh-confirm-actions">
|
||
<button class="lh-confirm-cancel" onclick="hideConfirmDelete()">Отмена</button>
|
||
<button class="lh-confirm-delete" id="confirm-del-btn" onclick="doDelete()">Удалить навсегда</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/api.js"></script>
|
||
<script src="/js/sidebar.js"></script>
|
||
<script src="/js/notifications.js"></script>
|
||
<script src="/js/mobile.js"></script>
|
||
<script src="/js/whiteboard.js"></script>
|
||
<script>
|
||
/* ══════════════════════════════════════════════════════
|
||
Lesson History — main script
|
||
══════════════════════════════════════════════════════ */
|
||
let _me = null;
|
||
let _isTeacher = false;
|
||
let _isAdmin = false;
|
||
let _classes = [];
|
||
let _sessions = [];
|
||
let _totalPages = 1;
|
||
let _curPage = 1;
|
||
let _classFilter = '';
|
||
let _teacherFilter = '';
|
||
let _dateFrom = '';
|
||
let _dateTo = '';
|
||
let _searchTimer = null;
|
||
let _sortMode = 'newest';
|
||
let _deleteId = null;
|
||
let _activeSession = null;
|
||
let _wb = null;
|
||
let _wbPages = [];
|
||
let _wbCurrentPage = 1;
|
||
|
||
/* ─── Init ─── */
|
||
(function() {
|
||
const { user } = LS.initPage();
|
||
if (!user) return;
|
||
_me = user;
|
||
_isTeacher = user.role === 'teacher' || user.role === 'admin';
|
||
_isAdmin = user.role === 'admin';
|
||
if (!_isTeacher) { location.replace('/my-lessons'); return; }
|
||
if (window.lucide) lucide.createIcons();
|
||
|
||
if (_isAdmin) {
|
||
// Show admin-specific UI
|
||
document.querySelector('.lh-wrap')?.classList.add('lh-admin');
|
||
document.getElementById('lh-teacher-filter').style.display = '';
|
||
document.getElementById('lh-class-filter').style.display = 'none'; // admin uses search instead
|
||
document.getElementById('lh-date-label').style.display = '';
|
||
document.getElementById('lh-date-label2').style.display = '';
|
||
document.getElementById('lh-date-from').style.display = '';
|
||
document.getElementById('lh-date-to').style.display = '';
|
||
document.getElementById('lh-search').placeholder = 'Поиск по названию, учителю, классу...';
|
||
document.getElementById('kpi-total-lbl').textContent = 'Всего уроков';
|
||
// Show "most_students" sort option
|
||
document.querySelector('.admin-opt')?.removeAttribute('style');
|
||
loadTeachersList();
|
||
} else {
|
||
loadClasses();
|
||
}
|
||
loadSessions();
|
||
checkUrlSession();
|
||
})();
|
||
|
||
function checkUrlSession() {
|
||
const sp = new URLSearchParams(location.search);
|
||
const sid = sp.get('session');
|
||
if (sid) setTimeout(() => openSession(Number(sid)), 600);
|
||
}
|
||
|
||
/* ─── Admin: teachers list ─── */
|
||
async function loadTeachersList() {
|
||
try {
|
||
const data = await LS.crAdminGetTeachersList();
|
||
const sel = document.getElementById('lh-teacher-filter');
|
||
(data.teachers || []).forEach(t => {
|
||
const opt = document.createElement('option');
|
||
opt.value = t.id; opt.textContent = t.name;
|
||
sel.appendChild(opt);
|
||
});
|
||
} catch {}
|
||
}
|
||
|
||
/* ─── Teacher: classes filter ─── */
|
||
async function loadClasses() {
|
||
try {
|
||
const data = await LS.getClasses();
|
||
_classes = data.classes || data || [];
|
||
const sel = document.getElementById('lh-class-filter');
|
||
_classes.forEach(c => {
|
||
const opt = document.createElement('option');
|
||
opt.value = c.id; opt.textContent = c.name;
|
||
sel.appendChild(opt);
|
||
});
|
||
} catch {}
|
||
}
|
||
|
||
function onClassFilter() {
|
||
_classFilter = document.getElementById('lh-class-filter').value;
|
||
_curPage = 1;
|
||
loadSessions();
|
||
}
|
||
|
||
function onTeacherFilter() {
|
||
_teacherFilter = document.getElementById('lh-teacher-filter').value;
|
||
_curPage = 1;
|
||
loadSessions();
|
||
}
|
||
|
||
function onDateFilter() {
|
||
_dateFrom = document.getElementById('lh-date-from').value;
|
||
_dateTo = document.getElementById('lh-date-to').value;
|
||
_curPage = 1;
|
||
loadSessions();
|
||
}
|
||
|
||
function onSearch() {
|
||
clearTimeout(_searchTimer);
|
||
_searchTimer = setTimeout(() => { _curPage = 1; loadSessions(); }, 350);
|
||
}
|
||
|
||
function onSort() {
|
||
_sortMode = document.getElementById('lh-sort').value;
|
||
if (_isAdmin) {
|
||
_curPage = 1;
|
||
loadSessions(); // server-side sort for admin
|
||
} else {
|
||
renderList(_sessions); // client-side sort for teacher
|
||
}
|
||
}
|
||
|
||
/* ─── Sessions list ─── */
|
||
async function loadSessions() {
|
||
const search = document.getElementById('lh-search').value.trim();
|
||
const list = document.getElementById('lh-list');
|
||
list.innerHTML = '<div class="lh-sk-card lh-skeleton"></div><div class="lh-sk-card lh-skeleton"></div><div class="lh-sk-card lh-skeleton"></div>';
|
||
|
||
try {
|
||
let data;
|
||
if (_isAdmin) {
|
||
data = await LS.crAdminGetAllHistory({
|
||
page: _curPage, search,
|
||
teacher: _teacherFilter,
|
||
class_id: '', // admin class filter via teacher filter + search
|
||
date_from: _dateFrom,
|
||
date_to: _dateTo,
|
||
sort: _sortMode,
|
||
});
|
||
updateKPIFromAgg(data.agg, data.total);
|
||
} else {
|
||
if (_classFilter) {
|
||
data = await LS.crGetClassHistory(Number(_classFilter), _curPage, search);
|
||
} else {
|
||
data = await LS.crGetMyHistory(_curPage);
|
||
}
|
||
updateKPI(data.sessions || []);
|
||
}
|
||
_sessions = data.sessions || [];
|
||
_totalPages = data.pages || 1;
|
||
renderList(_sessions);
|
||
renderPagination(_curPage, _totalPages, data.total || 0);
|
||
} catch (e) {
|
||
list.innerHTML = `<div class="lh-empty"><p>Ошибка загрузки: ${LS.escapeHtml(e.message || 'неизвестная ошибка')}</p></div>`;
|
||
}
|
||
}
|
||
|
||
function renderList(sessions) {
|
||
const list = document.getElementById('lh-list');
|
||
const badge = document.getElementById('lh-count-badge');
|
||
if (badge) badge.textContent = sessions.length ? plural(sessions.length, 'урок','урока','уроков') : '';
|
||
|
||
if (!sessions.length) {
|
||
list.innerHTML = `<div class="lh-empty">
|
||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>
|
||
<p>Завершённых уроков пока нет</p></div>`;
|
||
return;
|
||
}
|
||
|
||
// Sort
|
||
const sorted = [...sessions].sort((a, b) => {
|
||
if (_sortMode === 'oldest') return new Date(a.ended_at||0) - new Date(b.ended_at||0);
|
||
if (_sortMode === 'longest') {
|
||
const da = a.ended_at && a.created_at ? new Date(a.ended_at)-new Date(a.created_at) : 0;
|
||
const db_ = b.ended_at && b.created_at ? new Date(b.ended_at)-new Date(b.created_at) : 0;
|
||
return db_ - da;
|
||
}
|
||
return new Date(b.ended_at||0) - new Date(a.ended_at||0); // newest
|
||
});
|
||
|
||
// Group by month
|
||
const groups = [];
|
||
let lastKey = null;
|
||
sorted.forEach(s => {
|
||
const d = s.ended_at ? new Date(s.ended_at) : null;
|
||
const key = d ? d.toLocaleDateString('ru-RU', { month: 'long', year: 'numeric' }) : 'Неизвестно';
|
||
if (key !== lastKey) { groups.push({ key, items: [] }); lastKey = key; }
|
||
groups[groups.length-1].items.push(s);
|
||
});
|
||
|
||
list.innerHTML = groups.map(g =>
|
||
`<div class="lh-date-sep">${g.key}</div>` + g.items.map(s => renderCard(s)).join('')
|
||
).join('');
|
||
}
|
||
|
||
function renderCard(s) {
|
||
const dateStr = s.ended_at ? new Date(s.ended_at).toLocaleDateString('ru-RU', { day:'numeric', month:'short' }) : '—';
|
||
const dur = s.ended_at && s.created_at ? fmtDuration(Math.round((new Date(s.ended_at) - new Date(s.created_at)) / 1000)) : null;
|
||
const active = _activeSession?.session?.id === s.id ? ' active' : '';
|
||
const cls = s.class_name ? `<span class="lh-card-class">${LS.escapeHtml(s.class_name)}</span>` : '';
|
||
const teacher = _isAdmin && s.teacher_name
|
||
? `<span class="lh-card-teacher"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>${LS.escapeHtml(s.teacher_name)}</span>` : '';
|
||
const delBtn = _isTeacher
|
||
? `<button class="lh-card-del" onclick="event.stopPropagation();confirmDelete(${s.id})" title="Удалить урок">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
||
</button>` : '';
|
||
return `<div class="lh-session-card${active}" data-id="${s.id}" onclick="openSession(${s.id})">
|
||
${delBtn}
|
||
<div class="lh-card-row1">
|
||
<div class="lh-card-title">${LS.escapeHtml(s.title || 'Без названия')}</div>
|
||
<div class="lh-card-date">${dateStr}</div>
|
||
</div>
|
||
<div class="lh-card-row2">
|
||
${teacher}${cls}
|
||
${dur ? `<span class="lh-card-stat"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>${dur}</span>` : ''}
|
||
<span class="lh-card-stat"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>${s.participant_count || 0}</span>
|
||
${(s.message_count||0) > 0 ? `<span class="lh-card-stat"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>${s.message_count}</span>` : ''}
|
||
${(s.page_count||1) > 1 ? `<span class="lh-card-stat"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>${s.page_count} стр.</span>` : ''}
|
||
${_isAdmin && (s.stroke_count||0) > 0 ? `<span class="lh-card-stat"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>${s.stroke_count}</span>` : ''}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderPagination(page, total, count) {
|
||
const pg = document.getElementById('lh-pagination');
|
||
const prev = document.getElementById('lh-prev');
|
||
const next = document.getElementById('lh-next');
|
||
const info = document.getElementById('lh-page-info');
|
||
pg.style.display = total > 1 ? 'flex' : 'none';
|
||
prev.disabled = page <= 1;
|
||
next.disabled = page >= total;
|
||
info.textContent = `${page} / ${total}`;
|
||
}
|
||
|
||
function changePage(dir) {
|
||
_curPage = Math.max(1, Math.min(_totalPages, _curPage + dir));
|
||
loadSessions();
|
||
}
|
||
|
||
function updateKPI(sessions) {
|
||
const durSec = sessions.reduce((a, s) => {
|
||
if (!s.ended_at || !s.created_at) return a;
|
||
return a + Math.round((new Date(s.ended_at) - new Date(s.created_at)) / 1000);
|
||
}, 0);
|
||
const students = sessions.reduce((a, s) => a + (s.participant_count || 0), 0);
|
||
const msgs = sessions.reduce((a, s) => a + (s.message_count || 0), 0);
|
||
|
||
document.getElementById('kpi-total').textContent = sessions.length;
|
||
document.getElementById('kpi-time').textContent = fmtDuration(durSec);
|
||
document.getElementById('kpi-students').textContent = students;
|
||
document.getElementById('kpi-msgs').textContent = msgs;
|
||
}
|
||
|
||
function updateKPIFromAgg(agg, total) {
|
||
if (!agg) return;
|
||
document.getElementById('kpi-total').textContent = total ?? agg.total_sessions ?? '—';
|
||
document.getElementById('kpi-time').textContent = fmtDuration(agg.total_duration_sec || 0);
|
||
document.getElementById('kpi-teachers').textContent = agg.total_teachers ?? '—';
|
||
document.getElementById('kpi-students').textContent = agg.total_participants ?? '—';
|
||
document.getElementById('kpi-msgs').textContent = agg.total_messages ?? '—';
|
||
}
|
||
|
||
/* ─── Session detail ─── */
|
||
async function openSession(id) {
|
||
// Highlight card
|
||
document.querySelectorAll('.lh-session-card').forEach(el => {
|
||
el.classList.toggle('active', Number(el.dataset.id) === id);
|
||
});
|
||
// Show detail area
|
||
document.getElementById('lh-detail-empty').style.display = 'none';
|
||
document.getElementById('lh-detail-content').style.display = 'flex';
|
||
document.getElementById('lh-detail-content').style.flexDirection = 'column';
|
||
|
||
// Mobile: hide list
|
||
if (window.innerWidth <= 768) {
|
||
document.getElementById('lh-list-panel').classList.add('hidden-mobile');
|
||
}
|
||
|
||
// Update URL
|
||
const url = new URL(location.href);
|
||
url.searchParams.set('session', id);
|
||
history.replaceState({}, '', url);
|
||
|
||
// Reset lazy-load flags for new session
|
||
_chatLoaded = false; _attendLoaded = false; _notesLoaded = false;
|
||
|
||
// Load summary
|
||
try {
|
||
const data = await LS.crGetSessionSummary(id);
|
||
_activeSession = data;
|
||
renderDetailHeader(data);
|
||
_wbPages = data.pages || [];
|
||
_wbCurrentPage = 1;
|
||
// Default to board tab
|
||
switchTab('board');
|
||
renderThumbs();
|
||
loadBoardPage(1);
|
||
} catch (e) {
|
||
LS.toast('Ошибка загрузки урока: ' + (e.message || ''), 'error');
|
||
}
|
||
}
|
||
|
||
function renderDetailHeader(data) {
|
||
const s = data.session;
|
||
const st = data.stats;
|
||
document.getElementById('det-title').textContent = s.title || 'Без названия';
|
||
|
||
// Delete button — teacher sees it for own sessions; admin always
|
||
const delBtn = document.getElementById('det-del-btn');
|
||
if (delBtn) {
|
||
const canDel = _isTeacher && (_me?.role === 'admin' || s.teacher_id === _me?.id);
|
||
delBtn.classList.toggle('visible', canDel);
|
||
if (canDel) delBtn.onclick = () => confirmDelete(s.id);
|
||
}
|
||
|
||
const d = s.ended_at ? new Date(s.ended_at) : null;
|
||
const dateEl = document.getElementById('det-date');
|
||
dateEl.querySelector('.det-meta-txt')?.remove();
|
||
const dateTxt = document.createElement('span');
|
||
dateTxt.className = 'det-meta-txt';
|
||
dateTxt.textContent = d ? d.toLocaleDateString('ru-RU', { day:'numeric', month:'long', year:'numeric' }) : '—';
|
||
dateEl.appendChild(dateTxt);
|
||
|
||
const techEl = document.getElementById('det-teacher');
|
||
techEl.querySelector('.det-meta-txt')?.remove();
|
||
const techTxt = document.createElement('span');
|
||
techTxt.className = 'det-meta-txt';
|
||
techTxt.textContent = s.teacher_name || '';
|
||
techEl.appendChild(techTxt);
|
||
|
||
const clsEl = document.getElementById('det-class');
|
||
if (s.class_name) {
|
||
clsEl.style.display = 'flex';
|
||
clsEl.querySelector('.det-meta-txt')?.remove();
|
||
const clsTxt = document.createElement('span');
|
||
clsTxt.className = 'det-meta-txt';
|
||
clsTxt.textContent = s.class_name;
|
||
clsEl.appendChild(clsTxt);
|
||
} else {
|
||
clsEl.style.display = 'none';
|
||
}
|
||
|
||
// Stats chips
|
||
const chips = [];
|
||
if (st.duration_sec) chips.push(`<span class="lh-stat-chip"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>${fmtDuration(st.duration_sec)}</span>`);
|
||
if (st.participant_count) chips.push(`<span class="lh-stat-chip"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>${st.participant_count} уч.</span>`);
|
||
if (st.page_count) chips.push(`<span class="lh-stat-chip"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>${st.page_count} стр.</span>`);
|
||
if (st.stroke_count) chips.push(`<span class="lh-stat-chip"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>${st.stroke_count} штрихов</span>`);
|
||
if (st.message_count) chips.push(`<span class="lh-stat-chip"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>${st.message_count} сообщ.</span>`);
|
||
document.getElementById('det-stats').innerHTML = chips.join('');
|
||
}
|
||
|
||
function backToList() {
|
||
document.getElementById('lh-list-panel').classList.remove('hidden-mobile');
|
||
document.getElementById('lh-detail-content').style.display = 'none';
|
||
document.getElementById('lh-detail-empty').style.display = 'flex';
|
||
const url = new URL(location.href);
|
||
url.searchParams.delete('session');
|
||
history.replaceState({}, '', url);
|
||
}
|
||
|
||
/* ─── Tabs ─── */
|
||
function switchTab(name) {
|
||
['board','chat','attend','notes'].forEach(t => {
|
||
document.getElementById(`tab-${t}`)?.classList.toggle('active', t === name);
|
||
const tc = document.getElementById(`tc-${t}`);
|
||
if (tc) tc.style.display = t === name ? (t === 'board' || t === 'chat' ? 'flex' : 'block') : 'none';
|
||
});
|
||
if (name === 'chat' && _activeSession && !_chatLoaded) loadChat();
|
||
if (name === 'attend' && _activeSession && !_attendLoaded) loadAttendance();
|
||
if (name === 'notes' && _activeSession && !_notesLoaded) loadNotes();
|
||
}
|
||
let _chatLoaded = false, _attendLoaded = false, _notesLoaded = false;
|
||
|
||
/* ─── Board ─── */
|
||
function renderThumbs() {
|
||
const container = document.getElementById('lh-thumbs');
|
||
if (!container) return;
|
||
container.innerHTML = '';
|
||
// Popup uses CSS grid — wrap all items directly
|
||
container.style.cssText = 'display:contents';
|
||
|
||
_wbPages.forEach(p => {
|
||
const item = document.createElement('div');
|
||
item.className = 'lh-thumb-item' + (p.page_num === _wbCurrentPage ? ' active' : '');
|
||
item.dataset.page = p.page_num;
|
||
item.onclick = () => { loadBoardPage(p.page_num); lhClosePages(); };
|
||
|
||
const wrap = document.createElement('div');
|
||
wrap.className = 'lh-thumb-canvas-wrap';
|
||
const cvs = document.createElement('canvas');
|
||
cvs.width = 192; cvs.height = 108;
|
||
wrap.appendChild(cvs);
|
||
|
||
const footer = document.createElement('div');
|
||
footer.className = 'lh-thumb-footer';
|
||
const lbl = document.createElement('span');
|
||
lbl.className = 'lh-thumb-lbl';
|
||
lbl.textContent = p.name || `Стр. ${p.page_num}`;
|
||
const num = document.createElement('span');
|
||
num.className = 'lh-thumb-num';
|
||
num.textContent = p.page_num;
|
||
footer.append(lbl, num);
|
||
|
||
item.append(wrap, footer);
|
||
container.appendChild(item);
|
||
});
|
||
|
||
updateBoardTopbar();
|
||
}
|
||
|
||
let _pagesPopupOpen = false;
|
||
function lhTogglePages() {
|
||
_pagesPopupOpen ? lhClosePages() : lhOpenPages();
|
||
}
|
||
function lhOpenPages() {
|
||
_pagesPopupOpen = true;
|
||
document.getElementById('lh-pages-popup').classList.remove('hidden');
|
||
document.getElementById('lh-page-indicator').classList.add('open');
|
||
// Render all thumbnails in background now that popup is visible
|
||
if (_activeSession) _renderAllThumbs(_activeSession.session.id);
|
||
}
|
||
function lhClosePages() {
|
||
_pagesPopupOpen = false;
|
||
document.getElementById('lh-pages-popup')?.classList.add('hidden');
|
||
document.getElementById('lh-page-indicator')?.classList.remove('open');
|
||
}
|
||
|
||
async function loadBoardPage(pageNum) {
|
||
if (!_activeSession) return;
|
||
const sessionId = _activeSession.session.id;
|
||
_wbCurrentPage = pageNum;
|
||
|
||
// Highlight thumb + scroll into view
|
||
document.querySelectorAll('.lh-thumb-item').forEach(el => {
|
||
const active = Number(el.dataset.page) === pageNum;
|
||
el.classList.toggle('active', active);
|
||
if (active) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||
});
|
||
|
||
updateBoardTopbar();
|
||
|
||
const loading = document.getElementById('lh-board-loading');
|
||
const canvas = document.getElementById('lh-canvas');
|
||
loading.style.display = 'flex';
|
||
|
||
try {
|
||
const res = await LS.get(`/api/classroom/${sessionId}/strokes?page_num=${pageNum}`);
|
||
const strokes = res.strokes || [];
|
||
const template = res.template || 'blank';
|
||
|
||
// Init or reinit whiteboard (readOnly)
|
||
if (_wb) { _wb.destroy(); _wb = null; }
|
||
// Wait one frame so aspect-ratio layout is calculated before fit()
|
||
await new Promise(r => requestAnimationFrame(r));
|
||
_wb = new Whiteboard(canvas, {
|
||
readOnly: true,
|
||
onZoomChange: z => {
|
||
const el = document.getElementById('lh-zoom-lbl');
|
||
if (el) el.textContent = Math.round(z * 100) + '%';
|
||
},
|
||
});
|
||
_wb.setTemplate(template);
|
||
_wb.loadStrokes(strokes);
|
||
|
||
// Fit view to the actual stroke content (not the full 1920×1080 empty canvas)
|
||
_wb.zoomFitStrokes();
|
||
|
||
// Render thumbnail for this page
|
||
const thumbEl = document.querySelector(`.lh-thumb-item[data-page="${pageNum}"] canvas`);
|
||
if (thumbEl) _wb.renderThumbnail(thumbEl);
|
||
|
||
loading.style.display = 'none';
|
||
|
||
// Lazily render all other thumbnails in background
|
||
_renderAllThumbs(sessionId);
|
||
} catch (e) {
|
||
loading.innerHTML = '<span style="color:rgba(255,100,100,.7)">Ошибка загрузки страницы</span>';
|
||
}
|
||
}
|
||
|
||
function updateBoardTopbar() {
|
||
const cur = _wbCurrentPage;
|
||
const total = _wbPages.length;
|
||
const page = _wbPages.find(p => p.page_num === cur);
|
||
const idx = _wbPages.findIndex(p => p.page_num === cur) + 1;
|
||
const titleEl = document.getElementById('lh-page-title');
|
||
const totalEl = document.getElementById('lh-page-total');
|
||
const prevBtn = document.getElementById('lh-page-prev');
|
||
const nextBtn = document.getElementById('lh-page-next');
|
||
if (titleEl) titleEl.textContent = page?.name ? `${page.name}` : `Страница ${cur}`;
|
||
if (totalEl) totalEl.textContent = total > 1 ? `${idx} / ${total}` : '';
|
||
if (prevBtn) prevBtn.disabled = idx <= 1;
|
||
if (nextBtn) nextBtn.disabled = idx >= total;
|
||
}
|
||
|
||
function lhPageNav(dir) {
|
||
const idx = _wbPages.findIndex(p => p.page_num === _wbCurrentPage);
|
||
const next = _wbPages[idx + dir];
|
||
if (next) loadBoardPage(next.page_num);
|
||
}
|
||
|
||
let _thumbRenderJob = null;
|
||
async function _renderAllThumbs(sessionId) {
|
||
// Cancel previous job
|
||
const jobId = Date.now();
|
||
_thumbRenderJob = jobId;
|
||
|
||
for (const p of _wbPages) {
|
||
if (_thumbRenderJob !== jobId) return; // cancelled
|
||
if (p.page_num === _wbCurrentPage) continue; // already rendered
|
||
const thumbEl = document.querySelector(`.lh-thumb-item[data-page="${p.page_num}"] canvas`);
|
||
if (!thumbEl || thumbEl._rendered) continue;
|
||
try {
|
||
const res = await LS.get(`/api/classroom/${sessionId}/strokes?page_num=${p.page_num}`);
|
||
if (_thumbRenderJob !== jobId) return;
|
||
const tmpCvs = document.createElement('canvas');
|
||
tmpCvs.width = 192; tmpCvs.height = 108;
|
||
const tmpWb = new Whiteboard(tmpCvs, { readOnly: true });
|
||
tmpWb.setTemplate(res.template || 'blank');
|
||
tmpWb.loadStrokes(res.strokes || []);
|
||
tmpWb.renderThumbnail(thumbEl);
|
||
tmpWb.destroy();
|
||
thumbEl._rendered = true;
|
||
} catch {}
|
||
// Small yield between requests
|
||
await new Promise(r => setTimeout(r, 80));
|
||
}
|
||
}
|
||
|
||
function lhZoom(delta) {
|
||
if (!_wb) return;
|
||
_wb.zoomTo(Math.max(0.25, Math.min(4, (_wb._zoom || 1) + delta)));
|
||
document.getElementById('lh-zoom-lbl').textContent = Math.round(_wb._zoom * 100) + '%';
|
||
}
|
||
|
||
function lhZoomFit() {
|
||
if (!_wb) return;
|
||
_wb.zoomFitStrokes();
|
||
}
|
||
|
||
function exportBoardPage() {
|
||
if (!_wb) return;
|
||
_wb.exportPNG();
|
||
}
|
||
|
||
/* ─── Chat ─── */
|
||
async function loadChat() {
|
||
_chatLoaded = true;
|
||
const sessionId = _activeSession.session.id;
|
||
const container = document.getElementById('lh-chat-list');
|
||
const toolbar = document.getElementById('lh-chat-toolbar');
|
||
container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--lh-text-3)">Загрузка...</div>';
|
||
|
||
// Export button (teacher only)
|
||
if (_isTeacher) {
|
||
toolbar.innerHTML = `<a class="lh-chat-export-btn" href="/api/classroom/${sessionId}/chat/export" download>
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||
Экспортировать чат
|
||
</a>`;
|
||
}
|
||
|
||
try {
|
||
const data = await LS.crGetChat(sessionId);
|
||
const msgs = data.messages || [];
|
||
// Update tab count
|
||
const cnt = document.getElementById('tab-chat-count');
|
||
if (msgs.length) { cnt.textContent = msgs.length; cnt.style.display = 'inline'; }
|
||
|
||
if (!msgs.length) {
|
||
container.innerHTML = `<div class="lh-empty"><svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg><p>В этом уроке сообщений нет</p></div>`;
|
||
return;
|
||
}
|
||
container.innerHTML = msgs.map(m => renderMsg(m)).join('');
|
||
} catch (e) {
|
||
container.innerHTML = `<div class="lh-empty"><p>Ошибка загрузки чата</p></div>`;
|
||
}
|
||
}
|
||
|
||
function renderMsg(m) {
|
||
const color = strToColor(m.user_name || '');
|
||
const time = m.created_at ? new Date(m.created_at).toLocaleTimeString('ru-RU', { hour:'2-digit', minute:'2-digit' }) : '';
|
||
const pinned = m.pinned ? ' pinned' : '';
|
||
const avatarLetter = (m.user_name || '?')[0].toUpperCase();
|
||
let body = m.message ? `<div class="lh-msg-text">${LS.escapeHtml(m.message)}</div>` : '';
|
||
if (m.attachment_url) {
|
||
const isImg = /\.(jpe?g|png|gif|webp)(\?|$)/i.test(m.attachment_url);
|
||
if (isImg) body += `<img class="lh-msg-img" src="${LS.escapeHtml(m.attachment_url)}" alt="вложение" loading="lazy"/>`;
|
||
else body += `<a href="${LS.escapeHtml(m.attachment_url)}" target="_blank" style="font-size:.78rem;color:#9B5DE5"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:3px"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg> Вложение</a>`;
|
||
}
|
||
return `<div class="lh-msg${pinned}">
|
||
<div class="lh-msg-avatar" style="background:${color}">${avatarLetter}</div>
|
||
<div class="lh-msg-body">
|
||
<div class="lh-msg-head">
|
||
<span class="lh-msg-name">${LS.escapeHtml(m.user_name || '?')}</span>
|
||
<span class="lh-msg-time">${time}</span>
|
||
</div>
|
||
${body}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
/* ─── Attendance ─── */
|
||
async function loadAttendance() {
|
||
_attendLoaded = true;
|
||
const sessionId = _activeSession.session.id;
|
||
const tbody = document.getElementById('lh-attend-body');
|
||
const sumEl = document.getElementById('lh-attend-summary');
|
||
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:var(--lh-text-3);padding:24px">Загрузка...</td></tr>';
|
||
|
||
const attendance = _activeSession.attendance || [];
|
||
if (!attendance.length) {
|
||
sumEl.innerHTML = '';
|
||
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:var(--lh-text-3);padding:24px">Нет данных о посещаемости</td></tr>';
|
||
return;
|
||
}
|
||
|
||
const avgDur = attendance.filter(a => a.duration_sec).reduce((s,a) => s + a.duration_sec, 0) / (attendance.filter(a => a.duration_sec).length || 1);
|
||
sumEl.innerHTML = `
|
||
<div class="lh-attend-kpi"><div class="lh-attend-kpi-val">${attendance.length}</div><div class="lh-attend-kpi-lbl">Участников</div></div>
|
||
<div class="lh-attend-kpi"><div class="lh-attend-kpi-val">${fmtDuration(Math.round(avgDur))}</div><div class="lh-attend-kpi-lbl">Среднее время</div></div>
|
||
`;
|
||
|
||
tbody.innerHTML = attendance.map(a => {
|
||
const joined = a.joined_at ? new Date(a.joined_at).toLocaleTimeString('ru-RU', { hour:'2-digit', minute:'2-digit' }) : '—';
|
||
const left = a.left_at ? new Date(a.left_at).toLocaleTimeString('ru-RU', { hour:'2-digit', minute:'2-digit' }) : '—';
|
||
const dur = a.duration_sec ? `<span class="lh-attend-dur">${fmtDuration(a.duration_sec)}</span>` : '—';
|
||
return `<tr>
|
||
<td><strong>${LS.escapeHtml(a.user_name || '?')}</strong></td>
|
||
<td>${joined}</td><td>${left}</td><td>${dur}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
}
|
||
|
||
/* ─── Notes ─── */
|
||
async function loadNotes() {
|
||
_notesLoaded = true;
|
||
const sessionId = _activeSession.session.id;
|
||
const container = document.getElementById('lh-notes-list');
|
||
container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--lh-text-3)">Загрузка...</div>';
|
||
|
||
try {
|
||
if (_isTeacher) {
|
||
const data = await LS.crGetAllNotes(sessionId);
|
||
const notes = data.notes || [];
|
||
if (!notes.length) {
|
||
container.innerHTML = `<div class="lh-empty"><p>Ученики не оставили заметок</p></div>`;
|
||
return;
|
||
}
|
||
container.innerHTML = notes.map(n => `
|
||
<div class="lh-note-card">
|
||
<div class="lh-note-head">
|
||
<div class="lh-note-author">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||
${LS.escapeHtml(n.user_name || '?')}
|
||
</div>
|
||
<div class="lh-note-time">${n.updated_at ? new Date(n.updated_at).toLocaleDateString('ru-RU') : ''}</div>
|
||
</div>
|
||
<div class="lh-note-text">${LS.escapeHtml(n.content)}</div>
|
||
</div>`).join('');
|
||
} else {
|
||
// Student: own notes
|
||
const data = await LS.get(`/api/classroom/${sessionId}/notes`);
|
||
const content = data.content || '';
|
||
if (!content.trim()) {
|
||
container.innerHTML = `<div class="lh-empty"><p>Вы не оставили заметок к этому уроку</p></div>`;
|
||
return;
|
||
}
|
||
container.innerHTML = `<div class="lh-own-note-box">
|
||
<div class="lh-own-note-lbl">Мои заметки</div>
|
||
<div class="lh-note-text">${LS.escapeHtml(content)}</div>
|
||
</div>`;
|
||
}
|
||
} catch {
|
||
container.innerHTML = `<div class="lh-empty"><p>Ошибка загрузки заметок</p></div>`;
|
||
}
|
||
}
|
||
|
||
/* ─── Delete history ─── */
|
||
function confirmDelete(id) {
|
||
const sid = id ?? _activeSession?.session?.id;
|
||
if (!sid) return;
|
||
_deleteId = sid;
|
||
const session = _sessions.find(s => s.id === sid) || _activeSession?.session;
|
||
const name = session?.title || 'Без названия';
|
||
document.getElementById('confirm-sname').textContent = `"${name}"`;
|
||
document.getElementById('confirm-del-btn').disabled = false;
|
||
document.getElementById('lh-confirm-overlay').classList.remove('hidden');
|
||
}
|
||
|
||
function hideConfirmDelete() {
|
||
_deleteId = null;
|
||
document.getElementById('lh-confirm-overlay').classList.add('hidden');
|
||
}
|
||
|
||
async function doDelete() {
|
||
if (!_deleteId) return;
|
||
const btn = document.getElementById('confirm-del-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Удаление...';
|
||
try {
|
||
await LS.crDeleteHistory(_deleteId);
|
||
// Remove from local list
|
||
_sessions = _sessions.filter(s => s.id !== _deleteId);
|
||
renderList(_sessions);
|
||
updateKPI(_sessions);
|
||
// If deleted session was open, close detail
|
||
if (_activeSession?.session?.id === _deleteId) {
|
||
_activeSession = null;
|
||
_chatLoaded = false; _attendLoaded = false; _notesLoaded = false;
|
||
document.getElementById('lh-detail-empty').style.display = 'flex';
|
||
document.getElementById('lh-detail-content').style.display = 'none';
|
||
const url = new URL(location.href);
|
||
url.searchParams.delete('session');
|
||
history.replaceState({}, '', url);
|
||
}
|
||
hideConfirmDelete();
|
||
LS.toast('Урок удалён', 'success');
|
||
} catch (e) {
|
||
btn.disabled = false;
|
||
btn.textContent = 'Удалить навсегда';
|
||
LS.toast('Ошибка удаления: ' + (e.message || ''), 'error');
|
||
}
|
||
}
|
||
|
||
/* ─── Helpers ─── */
|
||
function fmtDuration(sec) {
|
||
if (!sec || sec < 0) return '0 мин';
|
||
const h = Math.floor(sec / 3600);
|
||
const m = Math.floor((sec % 3600) / 60);
|
||
if (h > 0) return `${h}ч ${m}м`;
|
||
if (m > 0) return `${m} мин`;
|
||
return `${sec} сек`;
|
||
}
|
||
|
||
function plural(n, one, few, many) {
|
||
const mod10 = n % 10, mod100 = n % 100;
|
||
if (mod10 === 1 && mod100 !== 11) return `${n} ${one}`;
|
||
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return `${n} ${few}`;
|
||
return `${n} ${many}`;
|
||
}
|
||
|
||
function strToColor(str) {
|
||
const colors = ['#9B5DE5','#06D6E0','#FF6B6B','#4361EE','#A8E063','#F15BB5','#FF9F43','#06D6A0'];
|
||
let h = 0;
|
||
for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) & 0xffff;
|
||
return colors[h % colors.length];
|
||
}
|
||
|
||
window.addEventListener('resize', () => {
|
||
if (window.innerWidth > 768) {
|
||
document.getElementById('lh-list-panel').classList.remove('hidden-mobile');
|
||
}
|
||
});
|
||
|
||
// Close pages popup when clicking outside
|
||
document.addEventListener('click', e => {
|
||
if (!_pagesPopupOpen) return;
|
||
const popup = document.getElementById('lh-pages-popup');
|
||
const btn = document.getElementById('lh-page-indicator');
|
||
if (popup && !popup.contains(e.target) && btn && !btn.contains(e.target)) {
|
||
lhClosePages();
|
||
}
|
||
});
|
||
|
||
// Keyboard navigation for board pages
|
||
document.addEventListener('keydown', e => {
|
||
if (!_activeSession || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||
const boardVisible = document.getElementById('tc-board')?.style.display !== 'none';
|
||
if (!boardVisible) return;
|
||
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); lhPageNav(-1); }
|
||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); lhPageNav(1); }
|
||
if (e.key === '0') lhZoomFit();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|