Rekkon

Rekkon

Инженер-конструктор / веб-программист
Пикабушник
Дата рождения: 14 мая
128 рейтинг 1 подписчик 2 подписки 2 поста 1 в горячем
5

Расширяем функционал приложения "Моя школа / электронный дневник" от Госуслуг⁠⁠

Серия Моя школа / Электронный дневник (от Госуслуг)

Продолжение. Начало тут: В помощь родителям которые мучаются с приложением "Моя школа / электронный дневник" от Госуслуг

Чтобы не повторяться опишу кратко ...
Вот стандартный вывод оценок ребенка:

стандартный интерфейс веб-приложения "Моя школа"

стандартный интерфейс веб-приложения "Моя школа"

Вот модифицированный моим скриптом дашборд статистики ребенка:

Также есть выгрузка в файлы Эксель (CSV). Парсер работает исключительно с разметкой страницы и никуда не лезет дальше окна веб-приложения.

Как установить в браузер (ничем не отличается от первой версии скрипта):

  1. Включите панель закладок: Ctrl+Shift+B.

  2. ПКМ по панели → «Добавить страницу» / «Добавить закладку»

  3. Имя: Назовите как вам удобно (У меня "Средний бал").

  4. URL: вставьте код который я приложил ниже.

  5. Сохранить → кликнуть по закладке на нужной странице.

Страницы обновленного дашборда успеваемости

Страницы обновленного дашборда успеваемости

Страницы обновленного дашборда успеваемости

Страницы обновленного дашборда успеваемости

Страницы обновленного дашборда успеваемости

Страницы обновленного дашборда успеваемости

Страницы обновленного дашборда успеваемости

Страницы обновленного дашборда успеваемости

Страницы обновленного дашборда успеваемости

Страницы обновленного дашборда успеваемости

Сам код (извините он очень объемный, так как скомпилирован для закладки браузера и убрать в спойлер его нельзя):


javascript:(function () { 'use strict'; var CFG = { TARGET: 4.5, STRICT: false, RECENT: 5, WEEKS: 16, DATE_HEADERS: '.date-item, [class*="date-item"]' }; var now = new Date(); var TODAY = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 12); var TGT_TXT = String(CFG.TARGET).replace('.', ','); var C = { ACC: '#2563eb', M5: '#16a34a', M4: '#65a30d', M3: '#d97706', M2: '#dc2626', INK: '#1f2937', MUT: '#64748b', GRID: '#e2e8f0', OK: '#15803d', WARN: '#b45309', BAD: '#b91c1c', NA: '#94a3b8' }; var WD = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; var MN = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек']; var MON3 = { янв: 1, фев: 2, мар: 3, апр: 4, май: 5, июн: 6, июл: 7, авг: 8, сен: 9, окт: 10, ноя: 11, дек: 12 }; function parseMark(raw) { return String(raw).split('/').map(function (s) { return parseInt(s, 10); }) .filter(function (n) { return n >= 1 && n <= 5; }); } function needFives(sum, n) { if (!n) return 0; var avg = sum / n; if (CFG.STRICT ? avg > CFG.TARGET : avg >= CFG.TARGET) return 0; var k = (CFG.TARGET * n - sum) / (5 - CFG.TARGET); return CFG.STRICT ? Math.floor(k + 1e-9) + 1 : Math.ceil(k - 1e-9); } function fmt(v) { return (v === null || v === undefined || isNaN(v)) ? '—' : Number(v).toFixed(2).replace('.', ','); } function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); } function avgColor(a) { return (a === null || isNaN(a)) ? C.NA : a >= CFG.TARGET ? C.OK : a >= 3.5 ? C.WARN : C.BAD; } function markColor(v) { return v === 5 ? C.M5 : v === 4 ? C.M4 : v === 3 ? C.M3 : C.M2; } function pad2(n) { return ('0' + n).slice(-2); } function sum(a) { return a.reduce(function (x, y) { return x + y; }, 0); } function goodDate(y, m, d) { if (!(y >= 1990 && y <= 2100 && m >= 1 && m <= 12 && d >= 1 && d <= 31)) return null; var dt = new Date(y, m - 1, d, 12); return (dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d) ? dt : null; } function parseHeader(text, guessYear) { var t = String(text).replace(/\s+/g, ' ').trim().toLowerCase(); if (!t) return null; var rel = /^сегодня/.test(t) ? 0 : /^вчера/.test(t) ? 1 : /^позавчера/.test(t) ? 2 : -1; if (rel >= 0) { var d = new Date(TODAY.getTime()); d.setDate(d.getDate() - rel); return d; } var m = t.match(/(\d{1,2})\s+([а-яё]{3,9})(?:[.,]|\s)+(?:(\d{4})\s*)?/); if (!m) m = t.match(/(\d{1,2})\s+([а-яё]{3,9})/); if (!m) return null; var day = +m[1], mon = MON3[m[2].slice(0, 3)], yr = m[3] ? +m[3] : null; if (!mon) return null; if (yr) return goodDate(yr, mon, day); if (guessYear) return goodDate(guessYear, mon, day); var asy = TODAY.getMonth() >= 7 ? TODAY.getFullYear() : TODAY.getFullYear() - 1; var y = mon >= 8 ? asy : asy + 1; var dt = goodDate(y, mon, day); if (dt && dt.getTime() > TODAY.getTime() + 86400000) dt = goodDate(y - 1, mon, day); return dt; } function dateMeta(dt) { var m = dt.getMonth() + 1, y = dt.getFullYear(); var q = (m === 9 || m === 10) ? 1 : (m === 11 || m === 12) ? 2 : (m <= 3) ? 3 : (m <= 5) ? 4 : 0; var monday = new Date(dt.getTime()); monday.setDate(monday.getDate() - ((dt.getDay() + 6) % 7)); return { ts: dt.getTime(), label: pad2(dt.getDate()) + '.' + pad2(m) + '.' + String(y).slice(2), monthKey: y + '-' + pad2(m), monthLabel: MN[m - 1] + ' ' + String(y).slice(2), quarter: q, wd: (dt.getDay() + 6) % 7, weekTs: monday.getTime(), weekLabel: pad2(monday.getDate()) + '.' + pad2(monday.getMonth() + 1) }; } function collect() { var recs = [], skipped = [], noSubj = 0, itemsCount = 0; var nodes = document.querySelectorAll(CFG.DATE_HEADERS + ', app-mark-item'); var curDate = null, prevTs = null; Array.prototype.forEach.call(nodes, function (node) { if (node.nodeType !== 1) return; if (node.matches(CFG.DATE_HEADERS)) { var gy = prevTs ? new Date(prevTs).getFullYear() : null; var dt = parseHeader(node.textContent, gy); if (dt) { while (prevTs && dt.getTime() > prevTs + 43200000) dt = new Date(dt.getFullYear() - 1, dt.getMonth(), dt.getDate(), 12); prevTs = dt.getTime(); curDate = dt; } return; } var item = node; itemsCount++; var inMk = function (el) { var x = el.closest('.mark'); return !!x && item.contains(x); }; var spans = Array.prototype.slice.call(item.querySelectorAll('span.title-h6')); var markEl = spans.filter(inMk)[0]; var subjEl = spans.filter(function (s) { return !inMk(s) && s.textContent.trim(); })[0]; if (!markEl || !subjEl) { noSubj++; return; } var subject = subjEl.textContent.replace(/\s+/g, ' ').trim(); var raw = (markEl.getAttribute('title') || markEl.textContent).replace(/\s+/g, ' ').trim(); var values = parseMark(raw); if (!values.length) { skipped.push(subject + ' — «' + raw + '»'); return; } var kindEl = item.querySelector('.small-text'); var kind = kindEl ? kindEl.textContent.replace(/\s+/g, ' ').trim() : '—'; var meta = curDate ? dateMeta(curDate) : null; values.forEach(function (v) { recs.push({ subject: subject, v: v, raw: raw, kind: kind, d: meta }); }); }); recs.forEach(function (r, i) { r.idx = i; }); return { recs: recs, skipped: skipped, noSubj: noSubj, itemsCount: itemsCount }; } function byKey(arr, keyFn) { var map = new Map(); arr.forEach(function (r) { var k = keyFn(r); if (k === null) return; if (!map.has(k)) map.set(k, []); map.get(k).push(r); }); return map; } function cntV(list, v) { return list.filter(function (r) { return r.v === v; }).length; } function avgV(list) { return list.length ? sum(list.map(function (r) { return r.v; })) / list.length : null; } function dayKey(ts) { var d = new Date(ts); return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()); } function buildModel(recs) { var M = {}; M.subjects = Array.from(byKey(recs, function (r) { return r.subject; }).entries()).map(function (e) { var name = e[0], list = e[1].slice().sort(function (a, b) { return ((a.d ? a.d.ts : 1e15) - (b.d ? b.d.ts : 1e15)) || (a.idx - b.idx); }); var s = sum(list.map(function (r) { return r.v; })); return { name: name, list: list, cnt: list.length, c5: cntV(list, 5), c4: cntV(list, 4), c3: cntV(list, 3), c2: cntV(list, 2), sum: s, avg: s / list.length, need: needFives(s, list.length), ctrl: list.filter(function (r) { return /контрольн/i.test(r.kind); }).length, trend: trendOf(list) }; }).sort(function (a, b) { return (b.avg - a.avg) || (b.cnt - a.cnt) || a.name.localeCompare(b.name, 'ru'); }); M.total = { cnt: recs.length, sum: sum(recs.map(function (r) { return r.v; })) }; M.total.avg = M.total.cnt ? M.total.sum / M.total.cnt : null; M.total.c5 = cntV(recs, 5); M.total.c4 = cntV(recs, 4); M.total.c3 = cntV(recs, 3); M.total.c2 = cntV(recs, 2); M.total.needAll = needFives(M.total.sum, M.total.cnt); var dated = recs.filter(function (r) { return !!r.d; }).sort(function (a, b) { return a.d.ts - b.d.ts || a.idx - b.idx; }); M.hasDates = dated.length > 0; M.dateShare = recs.length ? dated.length / recs.length : 0; M.minTs = dated.length ? dated[0].d.ts : null; M.maxTs = dated.length ? dated[dated.length - 1].d.ts : null; M.months = Array.from(byKey(dated, function (r) { return r.d.monthKey; }).entries()) .sort(function (a, b) { return a[0] < b[0] ? -1 : 1; }) .map(function (e) { var l = e[1]; return { key: e[0], label: l[0].d.monthLabel, cnt: l.length, sum: sum(l.map(function (r) { return r.v; })), avg: avgV(l), c5: cntV(l, 5), c4: cntV(l, 4), c3: cntV(l, 3), c2: cntV(l, 2) }; }); M.weeks = Array.from(byKey(dated, function (r) { return r.d.weekTs; }).entries()) .sort(function (a, b) { return a[0] - b[0]; }) .map(function (e) { var l = e[1]; return { ts: +e[0], label: l[0].d.weekLabel, cnt: l.length, avg: avgV(l) }; }); M.wd = []; for (var i = 0; i < 7; i++) M.wd.push({ list: [] }); dated.forEach(function (r) { M.wd[r.d.wd].list.push(r); }); M.wd = M.wd.map(function (w, i) { return { i: i, name: WD[i], cnt: w.list.length, avg: avgV(w.list) }; }); M.quarters = M.subjects.map(function (s) { var qs = [[], [], [], []]; s.list.forEach(function (r) { if (r.d && r.d.quarter >= 1) qs[r.d.quarter - 1].push(r); }); return { name: s.name, q: qs.map(avgV), avg: s.avg }; }); var allQ = [[], [], [], []]; dated.forEach(function (r) { if (r.d.quarter >= 1) allQ[r.d.quarter - 1].push(r); }); M.quartersTotal = allQ.map(avgV); M.kinds = Array.from(byKey(recs, function (r) { return r.kind; }).entries()) .map(function (e) { return { kind: e[0], cnt: e[1].length, avg: avgV(e[1]) }; }) .sort(function (a, b) { return b.cnt - a.cnt; }); var days = Array.from(new Set(dated.map(function (r) { return dayKey(r.d.ts); }))).sort(); M.daysWithMarks = days.length; var gap = { days: 0 }; for (var gi = 1; gi < days.length; gi++) { var diff = (new Date(days[gi]).getTime() - new Date(days[gi - 1]).getTime()) / 86400000; if (diff - 1 > gap.days) gap = { days: diff - 1, from: days[gi - 1], to: days[gi] }; } M.longestGap = gap; return M; } function trendOf(list) { var n = CFG.RECENT, vals = list.map(function (r) { return r.v; }); if (vals.length < 2) return null; var mid = vals.length >= n * 2 ? vals.length - n : Math.floor(vals.length / 2); var prev = vals.slice(Math.max(0, mid - n), mid), last = vals.slice(mid, mid + n); if (!prev.length || !last.length) return null; var aLast = sum(last) / last.length, aPrev = sum(prev) / prev.length, d = aLast - aPrev; return { sym: d > 0.15 ? '↑' : d < -0.15 ? '↓' : '→', cls: d > 0.15 ? C.OK : d < -0.15 ? C.BAD : C.NA, title: 'последние ' + last.length + ': ' + fmt(aLast) + ' · предыдущие ' + prev.length + ': ' + fmt(aPrev) }; } function svgOpen(w, h) { return '<svg viewBox="0 0 ' + w + ' ' + h + '" style="display:block;max-width:100%;height:auto" role="img">'; } function sparkline(vals) { var w = 118, h = 30, p = 4, s = ''; var yOf = function (v) { return h - p - Math.min(4, Math.max(0, v - 1)) / 4 * (h - 2 * p); }; s += svgOpen(w, h); s += '<title>' + esc(vals.join(', ')) + '</title>'; var gy = yOf(CFG.TARGET); s += '<line x1="' + p + '" y1="' + gy + '" x2="' + (w - p) + '" y2="' + gy + '" stroke="' + C.OK + '" stroke-width="1" stroke-dasharray="3 2" opacity="0.55"/>'; if (vals.length) { var pts = vals.map(function (v, i) { var x = vals.length === 1 ? w / 2 : p + (w - 2 * p) * i / (vals.length - 1); return [x, yOf(v)]; }); var line = pts.map(function (pt) { return pt[0].toFixed(1) + ',' + pt[1].toFixed(1); }).join(' '); if (vals.length > 1) s += '<polyline points="' + line + '" fill="none" stroke="' + C.ACC + '" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>'; pts.forEach(function (pt, i) { if (vals.length <= 30 || i === pts.length - 1) s += '<circle cx="' + pt[0].toFixed(1) + '" cy="' + pt[1].toFixed(1) + '" r="' + (i === pts.length - 1 ? 2.8 : 1.8) + '" fill="' + C.ACC + '"/>'; }); } return s + '</svg>'; } function lineChart(pts, goalLabel) { var W = 920, H = 240, L = 44, R = 16, T = 16, B = 34, s = ''; var vals = pts.map(function (p) { return p.val; }).concat([CFG.TARGET]); var mn = Math.max(1, Math.floor((Math.min.apply(null, vals) - 0.15) * 2) / 2); var mx = Math.min(5, Math.ceil((Math.max.apply(null, vals) + 0.15) * 2) / 2); if (mx - mn < 0.5) mx = mn + 0.5; var X = function (i) { return pts.length === 1 ? (L + W - R) / 2 : L + (W - L - R) * i / (pts.length - 1); }; var Y = function (v) { return T + (H - T - B) * (1 - (v - mn) / (mx - mn)); }; s += svgOpen(W, H); for (var g = mn; g <= mx + 1e-9; g += 0.5) { s += '<line x1="' + L + '" y1="' + Y(g) + '" x2="' + (W - R) + '" y2="' + Y(g) + '" stroke="' + C.GRID + '" stroke-width="1"/>'; s += '<text x="' + (L - 6) + '" y="' + (Y(g) + 3.5) + '" font-size="10" fill="' + C.MUT + '" text-anchor="end">' + fmt(g) + '</text>'; } s += '<line x1="' + L + '" y1="' + Y(CFG.TARGET) + '" x2="' + (W - R) + '" y2="' + Y(CFG.TARGET) + '" stroke="' + C.OK + '" stroke-width="1.4" stroke-dasharray="6 4" opacity="0.75"/>'; s += '<text x="' + (W - R) + '" y="' + (Y(CFG.TARGET) - 5) + '" font-size="10" fill="' + C.OK + '" text-anchor="end">' + esc(goalLabel) + '</text>'; if (pts.length > 1) { s += '<polyline points="' + pts.map(function (p, i) { return X(i).toFixed(1) + ',' + Y(p.val).toFixed(1); }).join(' ') + '" fill="none" stroke="' + C.ACC + '" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"/>'; } pts.forEach(function (p, i) { s += '<circle cx="' + X(i).toFixed(1) + '" cy="' + Y(p.val).toFixed(1) + '" r="4" fill="#fff" stroke="' + C.ACC + '" stroke-width="2"><title>' + esc(p.label) + ': ' + fmt(p.val) + ' (' + p.cnt + ' оц.)</title></circle>'; if (pts.length <= 14) s += '<text x="' + X(i).toFixed(1) + '" y="' + (Y(p.val) - 9).toFixed(1) + '" font-size="10.5" font-weight="700" fill="' + avgColor(p.val) + '" text-anchor="middle">' + fmt(p.val) + '</text>'; var rot = pts.length > 8, tx = X(i), ty = H - B + 15; s += '<text x="' + tx + '" y="' + ty + '" font-size="10" fill="' + C.MUT + '" text-anchor="' + (rot ? 'end' : 'middle') + '"' + (rot ? ' transform="rotate(-35 ' + tx + ' ' + ty + ')"' : '') + '>' + esc(p.label) + '</text>'; }); return s + '</svg>'; } function stackedMonths(months) { var W = 920, H = 210, L = 30, R = 12, T = 22, B = 34, s = ''; var maxT = Math.max.apply(null, months.map(function (m) { return m.cnt; }).concat([1])); var step = (W - L - R) / months.length, bw = Math.min(64, step * 0.62); var Y = function (v) { return H - B - (H - T - B) * v / maxT; }; s += svgOpen(W, H); [0, Math.ceil(maxT / 2), maxT].forEach(function (g) { s += '<line x1="' + L + '" y1="' + Y(g) + '" x2="' + (W - R) + '" y2="' + Y(g) + '" stroke="' + C.GRID + '" stroke-width="1"/>'; s += '<text x="' + (L - 5) + '" y="' + (Y(g) + 3.5) + '" font-size="10" fill="' + C.MUT + '" text-anchor="end">' + g + '</text>'; }); months.forEach(function (m, i) { var x = L + step * i + (step - bw) / 2, y0 = H - B; [[m.c5, C.M5], [m.c4, C.M4], [m.c3, C.M3], [m.c2, C.M2]].forEach(function (part) { if (!part[0]) return; var y1 = Y(part[0]) - (H - B) + y0; s += '<rect x="' + x.toFixed(1) + '" y="' + y1.toFixed(1) + '" width="' + bw.toFixed(1) + '" height="' + (y0 - y1).toFixed(1) + '" fill="' + part[1] + '" rx="1.5"/>'; y0 = y1; }); s += '<text x="' + (x + bw / 2).toFixed(1) + '" y="' + (y0 - 6).toFixed(1) + '" font-size="11" font-weight="700" fill="' + C.INK + '" text-anchor="middle">' + m.cnt + '</text>'; s += '<text x="' + (x + bw / 2).toFixed(1) + '" y="' + (H - B + 15) + '" font-size="10" fill="' + C.MUT + '" text-anchor="middle">' + esc(m.label) + '</text>'; }); return s + '</svg>'; } function donut(parts, centerBig, centerSmall) { var total = sum(parts.map(function (p) { return p.val; })) || 1; var used = parts.filter(function (p) { return p.val > 0; }); var s = svgOpen(150, 150), off = 25; if (used.length === 1) { s += '<circle cx="75" cy="75" r="62" fill="none" stroke="' + used[0].color + '" stroke-width="20"/>'; } else { s += '<circle cx="75" cy="75" r="62" fill="none" stroke="#f1f5f9" stroke-width="20"/>'; used.forEach(function (p) { var len = p.val / total * 389.6; s += '<circle cx="75" cy="75" r="62" fill="none" stroke="' + p.color + '" stroke-width="20" stroke-dasharray="' + (len - 1.5).toFixed(1) + ' ' + (389.6 - len + 1.5).toFixed(1) + '" stroke-dashoffset="' + off.toFixed(1) + '" transform="rotate(-90 75 75)"><title>' + esc(p.label) + ': ' + p.val + '</title></circle>'; off += len; }); } s += '<text x="75" y="76" font-size="26" font-weight="700" fill="' + C.INK + '" text-anchor="middle">' + esc(centerBig) + '</text>'; s += '<text x="75" y="94" font-size="10.5" fill="' + C.MUT + '" text-anchor="middle">' + esc(centerSmall) + '</text>'; return s + '</svg>'; } function weekActivity(weeks) { var W = 920, H = 200, L = 30, R = 40, T = 16, B = 60, s = ''; var maxC = Math.max.apply(null, weeks.map(function (w) { return w.cnt; }).concat([1])); var step = (W - L - R) / weeks.length, bw = Math.min(46, step * 0.66); var Y = function (v) { return H - B - (H - T - B) * v / maxC; }; var YA = function (v) { return H - B - (H - T - B) * (Math.min(5, Math.max(2, v)) - 2) / 3; }; s += svgOpen(W, H); [0, Math.ceil(maxC / 2), maxC].forEach(function (g) { s += '<line x1="' + L + '" y1="' + Y(g) + '" x2="' + (W - R) + '" y2="' + Y(g) + '" stroke="' + C.GRID + '" stroke-width="1"/>'; s += '<text x="' + (L - 5) + '" y="' + (Y(g) + 3.5) + '" font-size="10" fill="' + C.MUT + '" text-anchor="end">' + g + '</text>'; }); var avgs = []; weeks.forEach(function (w, i) { var x = L + step * i + (step - bw) / 2; s += '<rect x="' + x.toFixed(1) + '" y="' + Y(w.cnt).toFixed(1) + '" width="' + bw.toFixed(1) + '" height="' + (H - B - Y(w.cnt)).toFixed(1) + '" fill="' + C.ACC + '" opacity="0.85" rx="2.5"><title>неделя от ' + w.label + ': ' + w.cnt + ' оценок, ср. ' + fmt(w.avg) + '</title></rect>'; s += '<text x="' + (x + bw / 2).toFixed(1) + '" y="' + (Y(w.cnt) - 5).toFixed(1) + '" font-size="10" font-weight="700" fill="' + C.ACC + '" text-anchor="middle">' + w.cnt + '</text>'; var lblEvery = Math.ceil(weeks.length / 10); if (i % lblEvery === 0) { var tx = x + bw / 2, ty = H - B + 14; s += '<text x="' + tx + '" y="' + ty + '" font-size="9.5" fill="' + C.MUT + '" text-anchor="end" transform="rotate(-45 ' + tx + ' ' + ty + ')">' + esc(w.label) + '</text>'; } if (w.avg !== null) avgs.push([x + bw / 2, YA(w.avg), w]); }); if (avgs.length > 1) { s += '<polyline points="' + avgs.map(function (p) { return p[0].toFixed(1) + ',' + p[1].toFixed(1); }).join(' ') + '" fill="none" stroke="' + C.INK + '" stroke-width="1.6" opacity="0.65" stroke-dasharray="1 0"/>'; avgs.forEach(function (p) { s += '<circle cx="' + p[0].toFixed(1) + '" cy="' + p[1].toFixed(1) + '" r="3" fill="#fff" stroke="' + avgColor(p[2].avg) + '" stroke-width="2"><title>неделя от ' + p[2].label + ': ср. ' + fmt(p[2].avg) + '</title></circle>'; }); } [2, 5].forEach(function (g) { s += '<text x="' + (W - R + 5) + '" y="' + (YA(g) + 3.5) + '" font-size="10" fill="' + C.MUT + '">' + g + ',0</text>'; }); return s + '</svg>'; } function td(v, extra) { return '<td style="padding:6px 10px;border:1px solid ' + C.GRID + ';text-align:center;' + (extra || '') + '">' + v + '</td>'; } function th(v, left) { return '<th style="padding:7px 10px;border:1px solid ' + C.GRID + ';text-align:' + (left ? 'left' : 'center') + ';background:#f8fafc;white-space:nowrap">' + v + '</th>'; } function chip(v) { return '<span style="display:inline-block;min-width:20px;padding:1px 7px;border-radius:9px;background:' + markColor(v) + ';color:#fff;font-weight:700">' + v + '</span>'; } function card(label, value, sub, color) { return '<div style="flex:1 1 130px;background:#f8fafc;border:1px solid ' + C.GRID + ';border-radius:12px;padding:10px 12px">' + '<div style="font-size:11px;color:' + C.MUT + '">' + label + '</div>' + '<div style="font-size:22px;font-weight:800;color:' + (color || C.INK) + ';line-height:1.15;margin-top:2px">' + value + '</div>' + (sub ? '<div style="font-size:11px;color:' + C.MUT + ';margin-top:2px">' + sub + '</div>' : '') + '</div>'; } function section(t) { return '<div style="font-weight:700;font-size:13.5px;margin:16px 0 8px;color:' + C.INK + '">' + t + '</div>'; } function legend(items) { return '<div style="display:flex;gap:14px;flex-wrap:wrap;font-size:11.5px;color:' + C.MUT + ';margin-top:6px">' + items.map(function (it) { return '<span style="display:inline-flex;align-items:center;gap:5px"><span style="width:11px;height:11px;border-radius:3px;background:' + it[1] + ';display:inline-block"></span>' + esc(it[0]) + '</span>'; }).join('') + '</div>'; } function wdTiles(wd, compact) { var maxC = Math.max.apply(null, wd.map(function (w) { return w.cnt; }).concat([1])); return '<div style="display:flex;gap:6px">' + wd.map(function (w) { var a = 0.07 + 0.6 * (w.cnt / maxC); return '<div style="flex:1 1 0;background:rgba(37,99,235,' + a.toFixed(2) + ');border-radius:9px;padding:' + (compact ? '6px 2px' : '9px 4px') + ';text-align:center" title="' + w.name + ': ' + w.cnt + ' оценок, средний ' + fmt(w.avg) + '">' + '<div style="font-size:10.5px;color:' + C.MUT + '">' + w.name + '</div>' + '<div style="font-size:' + (compact ? 13 : 17) + 'px;font-weight:800;color:' + C.INK + '">' + w.cnt + '</div>' + (compact ? '' : '<div style="font-size:10.5px;font-weight:700;color:' + avgColor(w.avg) + '">' + fmt(w.avg) + '</div>') + '</div>'; }).join('') + '</div>'; } function tabOverview(M) { var h = '<div style="display:flex;gap:8px;flex-wrap:wrap">'; h += card('Средний балл', fmt(M.total.avg), 'цель ' + TGT_TXT, avgColor(M.total.avg)); h += card('Всего оценок', M.total.cnt, M.daysWithMarks ? M.daysWithMarks + ' дн. с оценками' : ''); h += card('Пятёрок', M.total.c5, 'четвёрок: ' + M.total.c4, C.M5); if (M.total.c3 + M.total.c2) h += card('Троек / двоек', M.total.c3 + ' / ' + M.total.c2, '', M.total.c2 ? C.BAD : C.WARN); h += card('Контрольных', sum(M.subjects.map(function (s) { return s.ctrl; })), 'работ'); if (M.subjects.length) { h += card('Лучший предмет', fmt(M.subjects[0].avg), esc(M.subjects[0].name), avgColor(M.subjects[0].avg)); var worst = M.subjects[M.subjects.length - 1]; if (worst !== M.subjects[0]) h += card('Требует внимания', fmt(worst.avg), esc(worst.name), avgColor(worst.avg)); } h += '</div>'; if (M.total.needAll > 0) h += '<div style="margin-top:10px;font-size:12px;color:' + C.MUT + '">До среднего ' + TGT_TXT + ' по всем предметам не хватает <b style="color:' + C.INK + '">' + M.total.needAll + '</b> пятёрок подряд (без других оценок).</div>'; h += '<div style="display:flex;gap:18px;flex-wrap:wrap;align-items:flex-start;margin-top:14px">'; var dist = donut([{ label: 'Пятёрки', val: M.total.c5, color: C.M5 }, { label: 'Четвёрки', val: M.total.c4, color: C.M4 }, { label: 'Тройки', val: M.total.c3, color: C.M3 }, { label: 'Двойки', val: M.total.c2, color: C.M2 }], String(M.total.cnt), 'оценок'); h += '<div style="flex:0 0 170px">' + dist + legend([['5', C.M5], ['4', C.M4], ['3', C.M3], ['2', C.M2]]) + '</div>'; h += '<div style="flex:1 1 320px"><div style="font-size:12px;color:' + C.MUT + ';margin-bottom:6px">Оценки по дням недели (число и средний балл)</div>' + wdTiles(M.wd, false) + '</div>'; h += '</div>'; if (M.kinds.length) { h += section('По типам работ'); h += '<table style="border-collapse:collapse;font-size:13px"><thead><tr>' + th('Тип работы', 1) + th('Оценок') + th('Средний балл') + '</tr></thead><tbody>'; M.kinds.forEach(function (k) { h += '<tr>' + td(esc(k.kind), 'text-align:left') + td(k.cnt) + td(fmt(k.avg), 'font-weight:700;color:' + avgColor(k.avg)) + '</tr>'; }); h += '</tbody></table>'; } return h; } function tabDynamics(M, noDatesNote) { if (!M.hasDates) return noDatesNote; var h = section('Средний балл по месяцам'); h += lineChart(M.months.map(function (m) { return { label: m.label, val: m.avg, cnt: m.cnt }; }), 'цель ' + TGT_TXT); h += section('Сколько оценок выставляли в месяц (по достоинству)'); h += stackedMonths(M.months); h += legend([['5', C.M5], ['4', C.M4], ['3', C.M3], ['2', C.M2]]); h += section('Средний балл по четвертям'); h += '<table style="border-collapse:collapse;font-size:13px"><thead><tr>' + th('Предмет', 1) + th('1 четв.') + th('2 четв.') + th('3 четв.') + th('4 четв.') + th('Весь период') + '</tr></thead><tbody>'; M.quarters.forEach(function (q) { h += '<tr>' + td(esc(q.name), 'text-align:left'); q.q.forEach(function (a) { h += td(fmt(a), 'font-weight:' + (a === null ? 400 : 700) + ';color:' + avgColor(a)); }); h += td(fmt(q.avg), 'font-weight:700;color:' + avgColor(q.avg)) + '</tr>'; }); h += '<tr style="background:#f8fafc;font-weight:700">' + td('ВСЕ ПРЕДМЕТЫ', 'text-align:left'); M.quartersTotal.forEach(function (a) { h += td(fmt(a), 'color:' + avgColor(a)); }); h += td(fmt(M.total.avg), 'color:' + avgColor(M.total.avg)) + '</tr>'; h += '</tbody></table>'; return h; } function tabSubjects(M) { var h = '<table style="border-collapse:collapse;width:100%;font-size:13px;min-width:860px"><thead><tr>' + th('Предмет', 1) + th('Всего') + th('5') + th('4') + th('3') + th('2') + th('Контр.') + th('Средний') + th('Тренд') + th('До ' + TGT_TXT) + th('Оценки по порядку') + '</tr></thead><tbody>'; M.subjects.forEach(function (s) { h += '<tr>' + td(esc(s.name), 'text-align:left') + td(s.cnt) + td(s.c5) + td(s.c4) + td(s.c3) + td(s.c2) + td(s.ctrl) + td(fmt(s.avg), 'font-weight:700;color:' + avgColor(s.avg)); h += td(s.trend ? '<span style="font-weight:800;color:' + s.trend.cls + '" title="' + esc(s.trend.title) + '">' + s.trend.sym + '</span>' : '—'); h += td(s.need ? String(s.need) : '—', 'font-weight:700') + td(sparkline(s.list.map(function (r) { return r.v; }))) + '</tr>'; }); h += '<tr style="background:#f8fafc;font-weight:700">' + td('ИТОГО', 'text-align:left') + td(M.total.cnt) + td(M.total.c5) + td(M.total.c4) + td(M.total.c3) + td(M.total.c2) + td(sum(M.subjects.map(function(s){return s.ctrl;}))) + td(fmt(M.total.avg), 'color:' + avgColor(M.total.avg)) + td('—') + td(M.total.needAll ? String(M.total.needAll) : '—') + td('') + '</tr>'; h += '</tbody></table>'; h += '<div style="margin-top:8px;font-size:11px;color:' + C.MUT + '">Тренд: средний из последних ' + CFG.RECENT + ' оценок против предыдущих ' + CFG.RECENT + '. Пунктир на мини-графике — цель ' + TGT_TXT + '. «Контр.» — число контрольных работ.</div>'; return h; } function tabActivity(M, noDatesNote) { if (!M.hasDates) return noDatesNote; var h = section('Оценки по неделям (столбики — количество, линия — средний балл)'); var weeks = M.weeks.slice(-CFG.WEEKS); h += weekActivity(weeks); if (M.weeks.length > weeks.length) h += '<div style="font-size:11px;color:' + C.MUT + ';margin-top:4px">Показаны последние ' + weeks.length + ' недель из ' + M.weeks.length + '.</div>'; h += section('По дням недели'); h += wdTiles(M.wd, false); h += section('Заметки'); h += '<ul style="margin:4px 0 0;padding-left:18px;font-size:12.5px;color:' + C.INK + '">'; var bestW = M.weeks.slice().sort(function (a, b) { return b.cnt - a.cnt; })[0]; if (bestW) h += '<li>Максимум оценок за неделю: <b>' + bestW.cnt + '</b> (неделя от ' + bestW.label + ').</li>'; h += '<li>В среднем оценок в неделю: <b>' + fmt(M.weeks.length ? sum(M.weeks.map(function (w) { return w.cnt; })) / M.weeks.length : null) + '</b>.</li>'; var bestD = M.wd.slice().sort(function (a, b) { return b.cnt - a.cnt; })[0]; if (bestD && bestD.cnt) h += '<li>Больше всего оценок ставят в <b>' + bestD.name + '</b> (' + bestD.cnt + '), средний балл в этот день — ' + fmt(bestD.avg) + '.</li>'; if (M.longestGap.days >= 2) h += '<li>Самый длинный перерыв без оценок: <b>' + M.longestGap.days + ' дн.</b> (между ' + M.longestGap.from.split('-').reverse().join('.') + ' и ' + M.longestGap.to.split('-').reverse().join('.') + ').</li>'; h += '</ul>'; return h; } function tabData(M, recs, skipped) { var rows = recs.slice().sort(function (a, b) { return ((a.d ? a.d.ts : 1e15) - (b.d ? b.d.ts : 1e15)) || (a.idx - b.idx); }); var h = '<div style="max-height:52vh;overflow:auto;border:1px solid ' + C.GRID + ';border-radius:10px">' + '<table style="border-collapse:collapse;font-size:12.5px;min-width:560px"><thead><tr>' + th('Дата') + th('День') + th('Предмет', 1) + th('Тип работы', 1) + th('Оценка') + '</tr></thead><tbody>'; rows.forEach(function (r) { h += '<tr>' + td(r.d ? r.d.label : '—') + td(r.d ? WD[r.d.wd] : '—') + td(esc(r.subject), 'text-align:left') + td(esc(r.kind), 'text-align:left;color:' + C.MUT) + td(chip(r.v)) + '</tr>'; }); h += '</tbody></table></div>'; h += '<div style="font-size:11px;color:' + C.MUT + ';margin-top:6px">Всего ' + rows.length + ' оценок (двойные вида «4/5» учтены как две).</div>'; if (skipped.length) h += '<div style="font-size:11px;color:' + C.WARN + ';margin-top:4px">Не учтены в среднем: ' + esc(skipped.join('; ')) + '.</div>'; return h; } function dl(name, lines) { var text = '%EF%BB%BF' + lines.join('\r\n'); var a = document.createElement('a'); if (window.URL && typeof URL.createObjectURL === 'function') { a.href = URL.createObjectURL(new Blob([text], { type: 'text/csv;charset=utf-8' })); } else { a.href = 'data:text/csv;charset=utf-8,' + encodeURIComponent(text); } a.download = name; document.body.appendChild(a); a.click(); a.remove(); } function csvSummary(M) { var L = ['Предмет;Всего;5;4;3;2;Контрольных;Средний балл;До ' + TGT_TXT]; M.subjects.forEach(function (s) { L.push([s.name, s.cnt, s.c5, s.c4, s.c3, s.c2, s.ctrl, fmt(s.avg), s.need].join(';')); }); L.push(['ИТОГО', M.total.cnt, M.total.c5, M.total.c4, M.total.c3, M.total.c2, sum(M.subjects.map(function(s){return s.ctrl;})), fmt(M.total.avg), ''].join(';')); return L; } function csvMonths(M) { var L = ['Месяц;Оценок;5;4;3;2;Средний балл']; M.months.forEach(function (m) { L.push([m.label, m.cnt, m.c5, m.c4, m.c3, m.c2, fmt(m.avg)].join(';')); }); return L; } function csvAll(recs) { var L = ['Дата;День недели;Предмет;Тип работы;Оценка']; recs.slice().sort(function (a, b) { return ((a.d ? a.d.ts : 1e15) - (b.d ? b.d.ts : 1e15)) || (a.idx - b.idx); }) .forEach(function (r) { L.push([r.d ? r.d.label : '', r.d ? WD[r.d.wd] : '', r.subject, r.kind, r.v].join(';')); }); return L; } function render(data, M, keepTab) { document.getElementById('marks-panel') && document.getElementById('marks-panel').remove(); var panel = document.createElement('div'); panel.id = 'marks-panel'; panel.style.cssText = 'position:fixed;top:14px;right:14px;left:auto;z-index:999999;background:#fff;border-radius:14px;box-shadow:0 14px 44px rgba(0,0,0,.3);padding:14px 16px 12px;font:14px/1.45 Arial,sans-serif;color:' + C.INK + ';max-height:94vh;max-width:95vw;width:1010px;overflow:auto'; var period = M.hasDates ? dateMeta(new Date(M.minTs)).label + ' — ' + dateMeta(new Date(M.maxTs)).label : 'даты не определены'; var TABS = [['view', 'Обзор'], ['dyn', 'Динамика'], ['subj', 'Предметы'], ['act', 'Активность'], ['data', 'Все оценки']]; var cur = keepTab || 'view'; var noDatesNote = '<div style="padding:18px 14px;background:#fffbeb;border:1px solid #fde68a;border-radius:10px;font-size:12.5px;color:#92400e">Даты у оценок не найдены — временной анализ недоступен. Скрипт ищет заголовки по селектору <b>' + esc(CFG.DATE_HEADERS) + '</b>. Если на странице они не совпадают, поправьте CFG.DATE_HEADERS в начале скрипта.</div>'; function headHtml() { var h = '<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px">'; h += '<div><div style="font-size:16px;font-weight:800">Дашборд успеваемости</div>'; h += '<div style="font-size:11.5px;color:' + C.MUT + ';margin-top:2px">Карточек: <b>' + data.itemsCount + '</b> · оценок: <b>' + M.total.cnt + '</b> · предметов: <b>' + M.subjects.length + '</b> · период: <b>' + period + '</b>'; if (M.hasDates && (TODAY - new Date(M.maxTs)) > 20 * 86400000) h += ' · <span style="color:' + C.WARN + '">данные старше 20 дней — обновите кнопкой ⟳ после прокрутки</span>'; if (M.hasDates && M.dateShare < 1) h += ' · <span style="color:' + C.WARN + '">у ' + Math.round((1 - M.dateShare) * M.total.cnt) + ' оценок дата не определена</span>'; h += '</div></div>'; h += '<div style="display:flex;gap:4px;flex:0 0 auto">' + '<button id="mp-refresh" title="Пересобрать (пролистайте страницу глубже и нажмите, чтобы подтянуть историю)" style="cursor:pointer;border:1px solid ' + C.GRID + ';background:#fff;border-radius:9px;font-size:17px;line-height:1;padding:4px 9px">⟳</button>' + '<button id="mp-close" title="Закрыть" style="cursor:pointer;border:1px solid ' + C.GRID + ';background:#fff;border-radius:9px;font-size:17px;line-height:1;padding:4px 9px">×</button></div></div>'; h += '<div style="display:flex;gap:6px;margin:10px 0 4px;flex-wrap:wrap">' + TABS.map(function (t) { var on = t[0] === cur; return '<button data-tab="' + t[0] + '" class="mp-tab" style="cursor:pointer;border:1px solid ' + (on ? C.ACC : C.GRID) + ';background:' + (on ? C.ACC : '#fff') + ';color:' + (on ? '#fff' : C.INK) + ';border-radius:999px;padding:5px 14px;font-size:13px;font-weight:' + (on ? 700 : 400) + '">' + t[1] + '</button>'; }).join('') + '</div>'; h += '<div style="display:flex;gap:8px;margin:8px 0 10px;flex-wrap:wrap;align-items:center">'; h += ['mp-csv1|Сводка CSV', 'mp-csv2|По месяцам CSV', 'mp-csv3|Все оценки CSV'].map(function (b) { var p = b.split('|'); return '<button id="' + p[0] + '" style="cursor:pointer;padding:5px 12px;border:1px solid ' + C.GRID + ';border-radius:8px;background:#fff;font-size:12px">' + p[1] + '</button>'; }).join(''); h += '<span style="font-size:11px;color:' + C.MUT + '">«До ' + TGT_TXT + '» — пятёрок подряд, чтобы средний стал ' + (CFG.STRICT ? '>' : '≥') + ' ' + TGT_TXT + '</span></div>'; return h; } function bodyHtml() { if (cur === 'view') return tabOverview(M); if (cur === 'dyn') return tabDynamics(M, noDatesNote); if (cur === 'subj') return tabSubjects(M); if (cur === 'act') return tabActivity(M, noDatesNote); return tabData(M, data.recs, data.skipped); } panel.innerHTML = headHtml() + '<div id="mp-body">' + bodyHtml() + '</div>' + '<div style="margin-top:10px;font-size:10.5px;color:#94a3b8">Всё считается локально, ничего никуда не отправляется · дашборд v2.0</div>'; document.body.appendChild(panel); Array.prototype.forEach.call(panel.querySelectorAll('.mp-tab'), function (b) { b.onclick = function () { cur = b.getAttribute('data-tab'); Array.prototype.forEach.call(panel.querySelectorAll('.mp-tab'), function (x) { var on = x === b; x.style.background = on ? C.ACC : '#fff'; x.style.color = on ? '#fff' : C.INK; x.style.borderColor = on ? C.ACC : C.GRID; x.style.fontWeight = on ? '700' : '400'; }); document.getElementById('mp-body').innerHTML = bodyHtml(); }; }); panel.querySelector('#mp-close').onclick = function () { panel.remove(); }; panel.querySelector('#mp-refresh').onclick = function () { run(cur); }; panel.querySelector('#mp-csv1').onclick = function () { dl('sredniy_ball.csv', csvSummary(M)); }; panel.querySelector('#mp-csv2').onclick = function () { dl('dinamika_po_mesyacam.csv', csvMonths(M)); }; panel.querySelector('#mp-csv3').onclick = function () { dl('vse_otsenki.csv', csvAll(data.recs)); }; } function run(keepTab) { var data = collect(); if (!data.recs.length) { alert('Оценки не найдены. Откройте страницу «Оценки», пролистайте её до конца нужного периода и запустите букмарклет снова.'); return; } var M = buildModel(data.recs); console.log('Карточек: ' + data.itemsCount + '; оценок: ' + M.total.cnt + '; предметов: ' + M.subjects.length + (M.hasDates ? '; период: ' + dateMeta(new Date(M.minTs)).label + ' — ' + dateMeta(new Date(M.maxTs)).label : '; даты не найдены')); if (data.skipped.length) console.log('Не учтены:', data.skipped); if (data.noSubj) console.log('Пропущено карточек без предмета/оценки:', data.noSubj); console.table(M.subjects.map(function (s) { return { 'Предмет': s.name, 'Всего': s.cnt, '5': s.c5, '4': s.c4, '3': s.c3, '2': s.c2, 'Контр.': s.ctrl, 'Средний балл': fmt(s.avg), 'Тренд': s.trend ? s.trend.sym : '', ['До ' + TGT_TXT]: s.need }; })); if (M.months.length) console.table(M.months.map(function (m) { return { 'Месяц': m.label, 'Оценок': m.cnt, '5': m.c5, '4': m.c4, '3': m.c3, '2': m.c2, 'Средний балл': fmt(m.avg) }; })); render(data, M, keepTab); } try { run(); } catch (e) { console.error(e); alert('Дашборд: ошибка — ' + (e && e.message ? e.message : e) + '\nПодробности в консоли (F12).'); } })();


Приятного пользования. И успехов в учёбе для ваших детей!

Показать полностью 6
25

В помощь родителям которые мучаются с приложением "Моя школа / электронный дневник" от Госуслуг⁠⁠

Серия Моя школа / Электронный дневник (от Госуслуг)

С этого года в школе у дочки сменили приложение для учебы. С хорошего на... скажем так, «специфическое». Не буду комментировать его общее качество и особенности, чтобы не разводить здесь многочасовой холивар — тема больная, а пост не об этом.

Оригинальный интерфейс

Оригинальный интерфейс

Но что меня безумно бесит в новом приложении, так это вывод таблицы оценок и функции подсчета среднего балла по предмету. Сделано это, мягко говоря, неудобно.

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

Почему это удобно:
Скрипт максимально простой. Чтобы его установить, не нужно быть программистом — любой желающий сможет добавить его в браузер буквально в пару кликов.

Теперь важный момент — ДИСКЛЕЙМЕР:

Я не нарушал и не планировал нарушать чьи-то интеллектуальные права!
Я не использую свой скрипт в коммерческих целях.
Это просто надстройка для более удобного и упорядоченного вывода данных с сайта.
Я не трогал исходный код приложения.
Я не хочу, чтобы меня забанили или заблокировали различные «озабоченные личности», имеющие отношение к системе.
Я даже не знаю, что ещё написать, чтобы ко мне не было никаких претензий! Просто хочу смотреть на оценки дочки без боли.

Вывод оценок после апгрэйда:

Модифицированный скрипт

Модифицированный скрипт

Как установить в браузер:

  1. Включите панель закладок: Ctrl+Shift+B.

  2. ПКМ по панели → «Добавить страницу» / «Добавить закладку»

  3. Имя: Назовите как вам удобно (У меня "Средний бал").

  4. URL: вставьте код который я приложил ниже.

  5. Сохранить → кликнуть по закладке на нужной странице.

Сам код:


javascript:(function(){const TARGET=4.5,STRICT=false;const tgt=String(TARGET).replace('.',',');const parseMark=raw=>raw.split('/').map(s=>parseInt(s,10)).filter(n=>n>=1&&n<=5);const needFives=(sum,n)=>{if(!n)return 0;const avg=sum/n;if(STRICT?avg>TARGET:avg>=TARGET)return 0;const k=(TARGET*n-sum)/(5-TARGET);return STRICT?Math.floor(k+1e-9)+1:Math.ceil(k-1e-9)};const items=document.querySelectorAll('app-mark-item');if(!items.length){alert('Оценки не найдены. Открой страницу с оценками, пролистай до конца и запусти снова.');return}const bySubject=new Map();const skipped=[];items.forEach(item=>{const inMark=el=>{const m=el.closest('.mark');return !!m&&item.contains(m)};const spans=[...item.querySelectorAll('span.title-h6')];const markEl=spans.find(inMark);const subjEl=spans.find(s=>!inMark(s));if(!markEl||!subjEl)return;const subject=subjEl.textContent.replace(/\s+/g,' ').trim();const raw=(markEl.getAttribute('title')||markEl.textContent).trim();const values=parseMark(raw);if(!bySubject.has(subject))bySubject.set(subject,[]);if(values.length)bySubject.get(subject).push(...values);else skipped.push(subject+' — «'+raw+'»')});const rows=[];const total={cnt:0,c5:0,c4:0,c3:0,c2:0,sum:0};for(const[subject,marks]of bySubject){const cnt=marks.length;const count=v=>marks.filter(m=>m===v).length;const sum=marks.reduce((a,b)=>a+b,0);const row={subject,cnt,c5:count(5),c4:count(4),c3:count(3),c2:count(2),avg:cnt?sum/cnt:null,need:needFives(sum,cnt)};rows.push(row);total.cnt+=cnt;total.sum+=sum;total.c5+=row.c5;total.c4+=row.c4;total.c3+=row.c3;total.c2+=row.c2}rows.sort((a,b)=>(b.avg??-1)-(a.avg??-1)||b.cnt-a.cnt||a.subject.localeCompare(b.subject,'ru'));const totalAvg=total.cnt?total.sum/total.cnt:null;const fmt=v=>v===null?'—':v.toFixed(2).replace('.',',');const esc=s=>String(s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));const color=a=>a===null?'#999':a>=TARGET?'#15803d':a>=3.5?'#b45309':'#b91c1c';const td=(v,extra='')=>'<td style="padding:7px 10px;border:1px solid #e5e7eb;text-align:center;'+extra+'">'+v+'</td>';console.log('Записей: '+items.length+'; оценок: '+total.cnt+'; предметов: '+rows.length);if(skipped.length)console.log('Не учтены:',skipped);console.table(rows.map(r=>({'Предмет':r.subject,'Всего':r.cnt,'5':r.c5,'4':r.c4,'3':r.c3,'2':r.c2,'Средний балл':fmt(r.avg),['До '+tgt]:r.need})));document.getElementById('marks-panel')?.remove();const panel=document.createElement('div');panel.id='marks-panel';panel.style.cssText='position:fixed;top:20px;right:20px;z-index:999999;background:#fff;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.25);padding:16px;font:14px/1.4 Arial,sans-serif;color:#222;max-height:90vh;overflow:auto;min-width:720px';const head=['Предмет','Всего','5','4','3','2','Средний балл','До '+tgt].map((h,i)=>'<th style="padding:8px 10px;border:1px solid #e5e7eb;text-align:'+(i?'center':'left')+'">'+h+'</th>').join('');const body=rows.map(r=>'<tr>'+td(esc(r.subject),'text-align:left')+td(r.cnt)+td(r.c5)+td(r.c4)+td(r.c3)+td(r.c2)+td(fmt(r.avg),'font-weight:700;color:'+color(r.avg))+td(r.need,'font-weight:700')+'</tr>').join('');const foot='<tr style="background:#f9fafb;font-weight:700">'+td('ИТОГО','text-align:left')+td(total.cnt)+td(total.c5)+td(total.c4)+td(total.c3)+td(total.c2)+td(fmt(totalAvg),'color:'+color(totalAvg))+td('—')+'</tr>';panel.innerHTML='<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px"><b style="font-size:15px">Средний балл по предметам</b><span id="marks-close" style="cursor:pointer;font-size:22px;line-height:1;padding:0 6px">×</span></div><table style="border-collapse:collapse;width:100%;font-size:13px"><thead><tr style="background:#f3f4f6">'+head+'</tr></thead><tbody>'+body+'</tbody><tfoot>'+foot+'</tfoot></table><div style="margin-top:10px;display:flex;gap:10px;align-items:center;flex-wrap:wrap"><button id="marks-csv" style="padding:6px 12px;border:1px solid #ddd;border-radius:8px;background:#fff;cursor:pointer">Скачать CSV</button><span style="color:#777;font-size:11px">«До '+tgt+'» — сколько пятёрок подряд нужно, чтобы средний стал '+(STRICT?'&gt;':'≥')+' '+tgt+'. '+(skipped.length?'В средний не вошли: '+esc(skipped.join(', '))+'.':'')+'</span></div>';document.body.appendChild(panel);document.getElementById('marks-close').onclick=()=>panel.remove();document.getElementById('marks-csv').onclick=()=>{const lines=['Предмет;Всего;5;4;3;2;Средний балл;До '+tgt].concat(rows.map(r=>[r.subject,r.cnt,r.c5,r.c4,r.c3,r.c2,fmt(r.avg),r.need].join(';'))).concat(['ИТОГО',total.cnt,total.c5,total.c4,total.c3,total.c2,fmt(totalAvg),''].join(';'));const a=document.createElement('a');a.href=URL.createObjectURL(new Blob(['\uFEFF'+lines.join('\r\n')],{type:'text/csv;charset=utf-8'}));a.download='sredniy_ball.csv';a.click()}})();


Приятного пользования. И успехов в учёбе для ваших детей!

UPD:

Добавил вторую часть поста с намного более расширенным функционалом:
Расширяем функционал приложения "Моя школа / электронный дневник" от Госуслуг

Показать полностью 2
Отличная работа, все прочитано!

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества

Недвижимость и ремонт

Теги

Популярные авторы

Сообщества