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)}
+
+
+ | ${esc(fa.label)} \\ ${esc(fb.label)} | ${cols.map(c=>`${esc(c)} | `).join('')}Всего |
+
+ ${rows.map(r=>{
+ const rowTotal=cols.reduce((s,c)=>s+(m[r][c]||0),0);
+ return `| ${esc(r)} | ${cols.map(c=>`${m[r][c]||0} | `).join('')}${rowTotal} |
`;
+ }).join('')}
+ | Всего | ${colTotals.map(c=>`${c} | `).join('')}${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}