From 3ec2a9a8d3c5f39c9635526b4c12d89695bde039 Mon Sep 17 00:00:00 2001 From: Dauren777 Date: Fri, 26 Jun 2026 06:43:56 +0000 Subject: [PATCH] =?UTF-8?q?v4:=20=D0=BC=D0=BD=D0=BE=D0=B3=D0=BE=D0=BF?= =?UTF-8?q?=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9=20=D0=BE=D1=82=D1=87=D1=91=D1=82=20?= =?UTF-8?q?=E2=80=94=20=D1=80=D0=B0=D1=81=D0=BF=D1=80=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=20=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D0=BA=D0=BE=D0=BB=D1=8C=D0=BA=D0=B8=D0=BC=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8F=D0=BC=20+=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D1=80?= =?UTF-8?q?=D1=91=D1=81=D1=82=D0=BD=D0=B0=D1=8F=20=D1=82=D0=B0=D0=B1=D0=BB?= =?UTF-8?q?=D0=B8=D1=86=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 166 ++++++++++++++++++++++++++++++++++++++--------------- index.html | 8 ++- styles.css | 19 +++++- 3 files changed, 140 insertions(+), 53 deletions(-) diff --git a/app.js b/app.js index 0397103..1ed6560 100644 --- a/app.js +++ b/app.js @@ -108,7 +108,6 @@ function bindEvents(){ document.getElementById('newFieldOptions').hidden = e.target.value!=='select'; }); document.getElementById('btnReportCsv').addEventListener('click', exportReportCSV); - document.getElementById('reportField').addEventListener('change', renderReports); // Фильтры document.getElementById('search').addEventListener('input', e=>{ filters.search=e.target.value.toLowerCase(); renderTable(); }); @@ -143,7 +142,7 @@ function uniqueValues(key){ } /* ---------- Рендер всего ---------- */ -function renderAll(){ renderStats(); renderDatalists(); renderTable(); buildFilterOptions(); buildReportFieldOptions(); } +function renderAll(){ renderStats(); renderDatalists(); renderTable(); buildFilterOptions(); renderReportFieldsList(); } /* ---------- Карточки статистики ---------- */ function renderStats(){ @@ -348,49 +347,52 @@ window.removeField = function(key){ save(); renderFieldList(); renderAll(); }; -/* ---------- Отчёты ---------- */ -function buildReportFieldOptions(){ - const sel = document.getElementById('reportField'); - const prev = sel.value; - sel.innerHTML = allFields().filter(f=>f.type!=='textarea').map(f=>``).join(''); - if(prev && [...sel.options].some(o=>o.value===prev)) sel.value=prev; +/* ---------- Отчёты (многопараметрические) ---------- */ +let reportSelected = new Set(['academicDegree','category']); // выбранные по умолчанию + +function renderReportFieldsList(){ + const box = document.getElementById('reportFieldsList'); + const fields = allFields().filter(f=>f.type!=='textarea'); + box.innerHTML = fields.map(f=>` + `).join(''); +} + +window.toggleReportField = function(key, on){ + if(on) reportSelected.add(key); else reportSelected.delete(key); + renderReports(); +}; + +function valNorm(t, f){ + let v = t[f.key]; + if(f.type==='checkbox') v = v ? 'Да':'Нет'; + if(v===undefined||v===null||v==='') v='(не указано)'; + return v; } function renderReports(){ - const sel = document.getElementById('reportField'); - const key = sel.value; - const f = allFields().find(x=>x.key===key); const grid = document.getElementById('reportGrid'); + const cross = document.getElementById('crosstabWrap'); const summary = document.getElementById('reportSummary'); if(state.teachers.length===0){ - grid.innerHTML=''; summary.innerHTML='

Нет данных для отчёта.

'; return; + grid.innerHTML=''; cross.innerHTML=''; summary.innerHTML='

Нет данных для отчёта.

'; return; } - // Группировка - const groups = {}; - state.teachers.forEach(t=>{ - let v = t[key]; - if(f && f.type==='checkbox') v = v ? 'Да':'Нет'; - if(v===undefined||v===null||v==='') v='(не указано)'; - groups[v] = (groups[v]||0)+1; - }); + const fields = allFields(); + const selected = fields.filter(f=>reportSelected.has(f.key) && f.type!=='textarea'); - const entries = Object.entries(groups).sort((a,b)=>b[1]-a[1]); - const max = Math.max(...entries.map(e=>e[1])); + // 1. Карточки распределения по каждому выбранному параметру + grid.innerHTML = selected.length + ? selected.map(distributionCard).join('') + : '

Отметьте параметры выше, чтобы построить отчёт.

'; - grid.innerHTML = ` -
-

${esc(f?f.label:key)} — распределение

- ${entries.map(([label,count])=>` -
- ${esc(label)} - - ${count} -
`).join('')} -
`; + // 2. Перекрёстная таблица по первым двум выбранным параметрам + cross.innerHTML = (selected.length>=2) ? crosstabHTML(selected[0], selected[1]) : ''; - // Сводка + // 3. Сводка const total = state.teachers.length; const withDegree = state.teachers.filter(t=>t.academicDegree && t.academicDegree!=='Нет').length; const pct = total?Math.round(withDegree/total*100):0; @@ -398,10 +400,54 @@ function renderReports(){

Сводка

Всего преподавателей: ${total}

С научной степенью: ${withDegree} (${pct}%)

-

Полей для параметров: ${allFields().length} (из них пользовательских: ${state.customFields.length})

-

Выберите поле выше, чтобы сгруппировать данные. Отчёт можно скачать в CSV.

`; +

Параметров в отчёте: ${selected.length}${selected.length>=2?` · перекрёстная таблица: ${esc(selected[0].label)} × ${esc(selected[1].label)}`:''}

+

Отметьте несколько параметров выше — по каждому будет свой график, а по первым двум — перекрёстная таблица. Отчёт скачивается в CSV целиком.

`; } +function distributionCard(f){ + const groups = {}; + state.teachers.forEach(t=>{ const v=valNorm(t,f); groups[v]=(groups[v]||0)+1; }); + const entries = Object.entries(groups).sort((a,b)=>b[1]-a[1]); + const max = Math.max(1, ...entries.map(e=>e[1])); + return `
+

${esc(f.label)} — распределение

+ ${entries.map(([label,count])=>` +
+ ${esc(label)} + + ${count} +
`).join('')} +
`; +} + +function crosstabHTML(fa, fb){ + const rows=[], cols=[], m={}; + state.teachers.forEach(t=>{ + const a=valNorm(t,fa), b=valNorm(t,fb); + if(!rows.includes(a)) rows.push(a); + if(!cols.includes(b)) cols.push(b); + m[a]=m[a]||{}; m[a][b]=(m[a][b]||0)+1; + }); + const colTotals = cols.map(c=>rows.reduce((s,r)=>s+(m[r][c]||0),0)); + const grand = colTotals.reduce((s,x)=>s+x,0); + return `
+

Перекрёстная таблица: ${esc(fa.label)} × ${esc(fb.label)}

+
+ + ${cols.map(c=>``).join('')} + + ${rows.map(r=>{ + const rowTotal=cols.reduce((s,c)=>s+(m[r][c]||0),0); + return `${cols.map(c=>``).join('')}`; + }).join('')} + ${colTotals.map(c=>``).join('')} + +
${esc(fa.label)} \\ ${esc(fb.label)}${esc(c)}Всего
${esc(r)}${m[r][c]||0}${rowTotal}
Всего${c}${grand}
+
+
`; +} + + /* ---------- Экспорт / Импорт ---------- */ function exportCSV(){ if(state.teachers.length===0){ alert('Нет данных для экспорта'); return; } @@ -419,19 +465,45 @@ function exportCSV(){ } function exportReportCSV(){ - const key = document.getElementById('reportField').value; - const f = allFields().find(x=>x.key===key); - if(!f){ alert('Выберите поле'); return; } - const groups = {}; - state.teachers.forEach(t=>{ - let v = t[key]; - if(f.type==='checkbox') v = v?'Да':'Нет'; - if(v===undefined||v===null||v==='') v='(не указано)'; - groups[v] = (groups[v]||0)+1; + const fields = allFields(); + const selected = fields.filter(f=>reportSelected.has(f.key) && f.type!=='textarea'); + if(selected.length===0){ alert('Отметьте хотя бы один параметр отчёта'); return; } + if(state.teachers.length===0){ alert('Нет данных для отчёта'); return; } + + const lines = []; + selected.forEach(f=>{ + const groups = {}; + state.teachers.forEach(t=>{ const v=valNorm(t,f); groups[v]=(groups[v]||0)+1; }); + const entries = Object.entries(groups).sort((a,b)=>b[1]-a[1]); + lines.push(`Отчёт по параметру: ${f.label}`); + lines.push(`${csvCell('Значение')};${csvCell('Количество')}`); + entries.forEach(([v,c])=>lines.push(`${csvCell(v)};${c}`)); + lines.push(''); // пустая строка-разделитель }); - const rows = Object.entries(groups).sort((a,b)=>b[1]-a[1]); - const csv = '\uFEFF' + [`${f.label};Количество`].concat(rows.map(r=>`${csvCell(r[0])};${r[1]}`)).join('\r\n'); - download(csv, `otchet_${f.label}.csv`, 'text/csv;charset=utf-8'); + + // Перекрёстная таблица по первым двум параметрам + if(selected.length>=2){ + const fa=selected[0], fb=selected[1]; + const rows=[], cols=[], m={}; + state.teachers.forEach(t=>{ + const a=valNorm(t,fa), b=valNorm(t,fb); + if(!rows.includes(a)) rows.push(a); + if(!cols.includes(b)) cols.push(b); + m[a]=m[a]||{}; m[a][b]=(m[a][b]||0)+1; + }); + lines.push(`Перекрёстная таблица: ${fa.label} × ${fb.label}`); + lines.push([csvCell(fa.label+' \\ '+fb.label)].concat(cols.map(csvCell)).concat([csvCell('Всего')]).join(';')); + rows.forEach(r=>{ + const rowTotal=cols.reduce((s,c)=>s+(m[r][c]||0),0); + lines.push([csvCell(r)].concat(cols.map(c=>m[r][c]||0)).concat([rowTotal]).join(';')); + }); + const colTotals=cols.map(c=>rows.reduce((s,r)=>s+(m[r][c]||0),0)); + const grand=colTotals.reduce((s,x)=>s+x,0); + lines.push([csvCell('Всего')].concat(colTotals).concat([grand]).join(';')); + } + + const csv = '\uFEFF' + lines.join('\r\n'); + download(csv, 'otchet.csv', 'text/csv;charset=utf-8'); } function csvCell(v){ diff --git a/index.html b/index.html index 0da14f5..f997def 100644 --- a/index.html +++ b/index.html @@ -62,12 +62,14 @@
- +
+ Параметры отчёта — отметьте несколько: +
+
+
diff --git a/styles.css b/styles.css index 00e988d..cc5127b 100644 --- a/styles.css +++ b/styles.css @@ -165,9 +165,13 @@ textarea.cell-input{resize:vertical;min-height:38px;line-height:1.4} .field-adder .btn{margin-top:6px} /* ===== Reports ===== */ -.report-controls{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-bottom:22px;background:#fff;border:1px solid var(--border);border-radius:var(--radius);padding:16px 20px;box-shadow:var(--shadow)} -.report-controls label{display:flex;align-items:center;gap:10px;font-size:14px;font-weight:600;color:var(--gray-700)} -.report-controls select.input{min-width:240px} +.report-controls{display:flex;flex-direction:column;gap:14px;margin-bottom:22px;background:#fff;border:1px solid var(--border);border-radius:var(--radius);padding:16px 20px;box-shadow:var(--shadow)} +.report-fields{display:flex;flex-direction:column;gap:8px} +.rf-title{font-size:14px;font-weight:600;color:var(--gray-700)} +.rf-list{display:flex;flex-wrap:wrap;gap:8px} +.rf-item{display:inline-flex;align-items:center;gap:7px;font-size:14px;cursor:pointer;background:var(--gray-50);border:1px solid var(--border);padding:6px 12px;border-radius:8px;user-select:none} +.rf-item input{cursor:pointer;accent-color:var(--cyan-strong)} +.rf-item:has(input:checked){background:var(--cyan-50);border-color:var(--cyan-strong)} .report-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px;margin-bottom:24px} .report-card{background:#fff;border:1px solid var(--border);border-radius:var(--radius);padding:18px 20px;box-shadow:var(--shadow)} .report-card h3{font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--gray-500);margin-bottom:14px} @@ -180,6 +184,15 @@ textarea.cell-input{resize:vertical;min-height:38px;line-height:1.4} .report-summary h3{font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--gray-500);margin-bottom:12px} .report-summary p{font-size:15px;line-height:1.7} .report-summary .big{font-size:22px;font-weight:800;color:var(--cyan-strong)} +.crosstab-wrap{margin-bottom:24px} +.crosstab{background:#fff;border:1px solid var(--border);border-radius:var(--radius);padding:18px 20px;box-shadow:var(--shadow)} +.crosstab h3{font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--gray-500);margin-bottom:14px} +.pivot{width:100%;border-collapse:collapse;font-size:13px} +.pivot th,.pivot td{padding:8px 12px;border:1px solid var(--border);text-align:center;white-space:nowrap} +.pivot th:first-child,.pivot td:first-child{text-align:left;font-weight:600} +.pivot thead th{background:var(--gray-50);font-size:12px;text-transform:uppercase;letter-spacing:.03em;color:var(--gray-500)} +.pivot .tot{font-weight:700;background:var(--gray-50)} +.pivot tr.total-row td{font-weight:700;background:var(--gray-100)} @media (max-width:760px){ .topbar h1{font-size:17px}