diff --git a/app.js b/app.js index 0694d96..0397103 100644 --- a/app.js +++ b/app.js @@ -98,13 +98,11 @@ function bindEvents(){ }); // Кнопки - document.getElementById('btnAdd').addEventListener('click', ()=>openEditor()); + document.getElementById('btnAdd').addEventListener('click', addRow); document.getElementById('btnFields').addEventListener('click', openFields); document.getElementById('btnExport').addEventListener('click', exportCSV); document.getElementById('btnImport').addEventListener('click', ()=>document.getElementById('importFile').click()); document.getElementById('importFile').addEventListener('change', importJSON); - document.getElementById('btnSave').addEventListener('click', saveEditor); - document.getElementById('btnDelete').addEventListener('click', deleteEditor); document.getElementById('btnAddField').addEventListener('click', addCustomField); document.getElementById('newFieldType').addEventListener('change', e=>{ document.getElementById('newFieldOptions').hidden = e.target.value!=='select'; @@ -118,11 +116,15 @@ function bindEvents(){ document.getElementById('filterCategory').addEventListener('change', e=>{ filters.category=e.target.value; renderTable(); }); document.getElementById('filterBase').addEventListener('change', e=>{ filters.base=e.target.value; renderTable(); }); + // Инлайн-редактирование таблицы (делегирование событий) + document.getElementById('tableBody').addEventListener('input', onCellInput); + document.getElementById('tableBody').addEventListener('change', onCellInput); + // Закрытие модалок по клику на фон document.querySelectorAll('.modal-overlay').forEach(ov=>{ ov.addEventListener('click', e=>{ if(e.target===ov) ov.hidden=true; }); }); - document.addEventListener('keydown', e=>{ if(e.key==='Escape'){ closeEditor(); closeFields(); } }); + document.addEventListener('keydown', e=>{ if(e.key==='Escape'){ closeFields(); } }); } /* ---------- Выпуски фильтров ---------- */ @@ -141,7 +143,7 @@ function uniqueValues(key){ } /* ---------- Рендер всего ---------- */ -function renderAll(){ renderStats(); renderTable(); buildFilterOptions(); } +function renderAll(){ renderStats(); renderDatalists(); renderTable(); buildFilterOptions(); buildReportFieldOptions(); } /* ---------- Карточки статистики ---------- */ function renderStats(){ @@ -164,26 +166,24 @@ function statCard(label,value,sub,cls){ return `
${label}
${value}
${sub||''}
`; } -/* ---------- Таблица ---------- */ +/* ---------- Таблица (инлайн-редактирование) ---------- */ function renderTable(){ const head = document.getElementById('tableHead'); const body = document.getElementById('tableBody'); const empty = document.getElementById('emptyState'); + const fields = allFields(); - // Колонки: ФИО, Степень, Категория, База, Сертификат, действия - head.innerHTML = ` - ФИОНаучная степеньКатегория - Клиническая базаСертификатДействия`; + // Заголовок: все поля + колонка действий + head.innerHTML = fields.map(f=>`${esc(f.label)}${f.required?' *':''}`).join('') + ''; const rows = filtered(); if(rows.length===0){ document.getElementById('teacherTable').style.display = state.teachers.length===0 ? 'none':'table'; - empty.hidden = state.teachers.length!==0; if(state.teachers.length!==0){ empty.hidden=false; empty.querySelector('p').textContent='Ничего не найдено по фильтрам.'; empty.querySelector('button').style.display='none'; } else { - empty.querySelector('p').textContent='Пока нет ни одного преподавателя.'; + empty.hidden=false; empty.querySelector('p').textContent='Пока нет ни одного преподавателя.'; empty.querySelector('button').style.display=''; } document.getElementById('countPill').textContent=''; @@ -194,15 +194,11 @@ function renderTable(){ empty.hidden=true; body.innerHTML = rows.map(t=>` - - ${esc(t.fullName||'—')} - ${degreeBadge(t.academicDegree)} - ${t.category && t.category!=='Нет' ? `${esc(t.category)}` : ''} - ${esc(t.clinicalBase||'—')} - ${t.specialistCertificate ? `✓ есть` : ''} + + ${fields.map(f=>`${cellInput(f,t)}`).join('')}
- +
@@ -211,6 +207,70 @@ function renderTable(){ document.getElementById('countPill').textContent = `Показано: ${rows.length} из ${state.teachers.length}`; } +// Поле ввода прямо в ячейке таблицы +function cellInput(f, t){ + const val = t[f.key] ?? ''; + const name = f.key; + if(f.type==='textarea') + return ``; + if(f.type==='date') + return ``; + if(f.type==='number') + return ``; + if(f.type==='checkbox') + return ``; + if(f.type==='select') + return ``; + return ``; +} + +// Один на каждое поле-список (без дублей по строкам) +function renderDatalists(){ + const box = document.getElementById('datalists'); + box.innerHTML = allFields().filter(f=>f.type==='select') + .map(f=>`${(f.options||[]).map(o=>``).join(''); +} + +// Автосохранение при вводе в любую ячейку +let _saveTimer=null, _statTimer=null; +function onCellInput(e){ + const el = e.target; + if(!el.classList || !el.classList.contains('cell-input')) return; + const tr = el.closest('tr'); if(!tr) return; + const id = tr.dataset.id; + const key = el.name; + const t = state.teachers.find(x=>x.id===id); if(!t) return; + t[key] = (el.type==='checkbox') ? el.checked : el.value; + clearTimeout(_saveTimer); _saveTimer=setTimeout(save, 350); + clearTimeout(_statTimer); _statTimer=setTimeout(()=>{ + renderStats(); + if(document.getElementById('tab-reports').classList.contains('active')) renderReports(); + }, 500); +} + +// Добавить пустую строку-преподавателя +function addRow(){ + const t = { id: uid() }; + allFields().forEach(f=>{ t[f.key] = (f.type==='checkbox') ? false : ''; }); + state.teachers.push(t); + // сбросить фильтры, чтобы новая строка была видна + filters = { search:'', degree:'', category:'', base:'' }; + document.getElementById('search').value=''; + ['filterDegree','filterCategory','filterBase'].forEach(id=>document.getElementById(id).value=''); + save(); buildFilterOptions(); renderTable(); renderStats(); + const tr = document.querySelector(`tr[data-id="${t.id}"]`); + if(tr){ const inp = tr.querySelector('[name="fullName"]'); if(inp) inp.focus(); } +} + +// Скачать анкету одного преподавателя (для отправки администратору) +window.exportTeacher = function(id){ + const t = state.teachers.find(x=>x.id===id); + if(!t) return; + const data = { teachers:[{ ...t }], customFields: state.customFields }; + download(JSON.stringify(data, null, 2), `anketa_${(t.fullName||'prepodavatel').replace(/\s+/g,'_')}.json`, 'application/json'); +}; + + function filtered(){ return state.teachers.filter(t=>{ if(filters.degree && t.academicDegree!==filters.degree) return false; @@ -225,13 +285,6 @@ function filtered(){ }); } -function degreeBadge(d){ - if(d==='Доктор наук') return 'Доктор наук'; - if(d==='Кандидат наук') return 'Кандидат наук'; - if(d && d!=='Нет') return `${esc(d)}`; - return 'нет'; -} - window.removeTeacher = function(id){ const t = state.teachers.find(x=>x.id===id); if(!t) return; @@ -241,82 +294,6 @@ window.removeTeacher = function(id){ } }; -/* ---------- Редактор (модалка) ---------- */ -window.openEditor = function(id){ - state.editingId = id || null; - const t = id ? state.teachers.find(x=>x.id===id) : null; - document.getElementById('editorTitle').textContent = t ? 'Редактировать преподавателя' : 'Новый преподаватель'; - document.getElementById('btnDelete').hidden = !t; - - const form = document.getElementById('editorForm'); - form.innerHTML = '
' + allFields().map(f=>fieldInput(f, t)).join('') + '
'; - - document.getElementById('editorModal').hidden=false; -}; - -function fieldInput(f, t){ - const val = t ? (t[f.key] ?? '') : ''; - const req = f.required ? '*' : ''; - const full = (f.type==='textarea') ? ' full' : ''; - let control=''; - if(f.type==='text' || f.type==='date' || f.type==='number'){ - control = ``; - } else if(f.type==='textarea'){ - control = ``; - } else if(f.type==='select'){ - // Редактируемый комбобокс: можно выбрать из подсказок или ввести своё значение - control = ` - ${(f.options||[]).map(o=>``; - } else if(f.type==='checkbox'){ - control = ``; - } - return `
- - ${control} -
`; -} - -function closeEditor(){ document.getElementById('editorModal').hidden=true; state.editingId=null; } - -function saveEditor(){ - const form = document.getElementById('editorForm'); - const data = {}; - let firstError=null; - for(const f of allFields()){ - const el = form.elements[f.key]; - if(!el){ data[f.key]=''; continue; } - if(f.type==='checkbox'){ - data[f.key] = el.checked; - } else { - data[f.key] = el.value.trim(); - } - if(f.required && !data[f.key] && data[f.key]!==false){ - if(!firstError) firstError=f.label; - } - } - if(firstError){ - alert('Заполните обязательное поле: '+firstError); - return; - } - if(state.editingId){ - const idx = state.teachers.findIndex(t=>t.id===state.editingId); - state.teachers[idx] = { ...state.teachers[idx], ...data }; - } else { - data.id = uid(); - state.teachers.push(data); - } - save(); closeEditor(); renderAll(); -} - -function deleteEditor(){ - if(!state.editingId) return; - const t = state.teachers.find(x=>x.id===state.editingId); - if(t && confirm(`Удалить преподавателя «${t.fullName}»?`)){ - state.teachers = state.teachers.filter(x=>x.id!==state.editingId); - save(); closeEditor(); renderAll(); - } -} - /* ---------- Настройка полей ---------- */ function openFields(){ renderFieldList(); @@ -359,7 +336,7 @@ function addCustomField(){ document.getElementById('newFieldOptions').value=''; document.getElementById('newFieldOptions').hidden=true; renderFieldList(); - buildFilterOptions(); + renderAll(); } window.removeField = function(key){ @@ -372,12 +349,15 @@ window.removeField = function(key){ }; /* ---------- Отчёты ---------- */ +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; +} + function renderReports(){ const sel = document.getElementById('reportField'); - // Заполняем список полей один раз - if(sel.options.length===0){ - sel.innerHTML = allFields().filter(f=>f.type!=='textarea').map(f=>``).join(''); - } const key = sel.value; const f = allFields().find(x=>x.key===key); const grid = document.getElementById('reportGrid'); @@ -466,15 +446,34 @@ function importJSON(e){ const reader = new FileReader(); reader.onload = ev=>{ try{ - const data = JSON.parse(ev.target.result); - if(!Array.isArray(data.teachers)) throw new Error('Нет массива teachers'); - if(!confirm(`Импортировать ${data.teachers.length} преподавателей? Текущие данные будут заменены.`)) return; - state.teachers = data.teachers; - state.customFields = data.customFields || []; - save(); renderAll(); buildFilterOptions(); - // сбросить список полей отчёта - document.getElementById('reportField').innerHTML=''; - alert('Импорт выполнен'); + const parsed = JSON.parse(ev.target.result); + // Нормализация: принимаем {teachers:[...]}, [...] или один объект-преподаватель + let incoming = [], extraFields = []; + if(Array.isArray(parsed)) incoming = parsed; + else if(Array.isArray(parsed.teachers)) { incoming = parsed.teachers; extraFields = parsed.customFields || []; } + else if(parsed && typeof parsed==='object') incoming = [parsed]; + else throw new Error('Не распознан формат файла'); + incoming = incoming.filter(t=>t && typeof t==='object'); + if(incoming.length===0) throw new Error('В файле нет данных преподавателей'); + + // Слияние пользовательских полей + extraFields.forEach(f=>{ if(f && f.key && !allFields().some(x=>x.key===f.key)) state.customFields.push(f); }); + + // Добавляем преподавателей, пропуская дубли по ФИО + const existing = new Set(state.teachers.map(t=>(t.fullName||'').trim().toLowerCase())); + let added = 0, skipped = 0; + incoming.forEach(t=>{ + const name = (t.fullName||'').trim().toLowerCase(); + if(name && existing.has(name)){ skipped++; return; } + const obj = { id: uid() }; + allFields().forEach(f=>{ obj[f.key] = t[f.key] ?? (f.type==='checkbox'? false : ''); }); + state.teachers.push(obj); + existing.add(name); + added++; + }); + + save(); renderAll(); + alert(`Добавлено: ${added}${skipped?`, пропущено дублей по ФИО: ${skipped}`:''}.`); }catch(err){ alert('Ошибка импорта: '+err.message); } }; reader.readAsText(file); diff --git a/index.html b/index.html index 6b8d447..0da14f5 100644 --- a/index.html +++ b/index.html @@ -53,9 +53,10 @@ +

ℹ Данные сохраняются в этом браузере. Чтобы собрать анкеты нескольких преподавателей: каждый вводит свою строку и жмёт в конце строки → отправляет файл вам → вы жмёте ⤒ Импорт (данные добавятся к таблице).

@@ -71,22 +72,6 @@ - - -