ty-produktovyy-dizayner-i-se/js/ui.js

324 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* HR Candidate Matcher — UI Layer */
/* global window, document */
(function () {
'use strict';
var APP = window.HRMatcher = window.HRMatcher || {};
var $ = function (sel) { return document.querySelector(sel); };
var $$ = function (sel) { return Array.prototype.slice.call(document.querySelectorAll(sel)); };
var toastTimer = null;
var confirmCallback = null;
var currentFilter = 'all';
var criteriaEditState = [];
APP.ui = {
showToast: function (message, type) {
var container = $('#toast-container');
if (!container) return;
var toast = document.createElement('div');
toast.className = 'toast' + (type ? ' toast-' + type : '');
toast.textContent = message;
container.appendChild(toast);
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toast.classList.add('toast-out');
setTimeout(function () { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 200);
}, 3000);
},
showConfirm: function (message, onConfirm) {
var modal = $('#modal-confirm');
var desc = $('#confirm-desc');
if (!modal || !desc) return;
desc.textContent = message;
modal.hidden = false;
confirmCallback = onConfirm;
$('#btn-confirm-ok').focus();
},
hideConfirm: function () {
var modal = $('#modal-confirm');
if (modal) modal.hidden = true;
confirmCallback = null;
},
resolveConfirm: function () {
if (confirmCallback) confirmCallback();
},
showError: function (fieldId, message) {
var field = $('#field-' + fieldId);
var error = $('#' + fieldId + '-error');
if (field) field.classList.add('has-error');
if (error) {
error.textContent = message;
error.hidden = false;
}
},
clearError: function (fieldId) {
var field = $('#field-' + fieldId);
var error = $('#' + fieldId + '-error');
if (field) field.classList.remove('has-error');
if (error) {
error.textContent = '';
error.hidden = true;
}
},
clearAllErrors: function () {
$$('.field').forEach(function (f) { f.classList.remove('has-error'); });
$$('.field-error').forEach(function (e) { e.textContent = ''; e.hidden = true; });
},
showScreen: function (screenName) {
$$('.screen').forEach(function (s) { s.hidden = true; });
var screen = $('#screen-' + screenName);
if (screen) {
screen.hidden = false;
screen.removeAttribute('hidden');
}
var nav = $('#app-nav');
var clearBtn = $('#btn-clear-all');
if (screenName === 'welcome') {
if (nav) nav.hidden = true;
if (clearBtn) clearBtn.hidden = true;
} else {
if (nav) nav.hidden = false;
if (clearBtn) clearBtn.hidden = false;
}
this.updateStepsIndicator(screenName);
window.scrollTo(0, 0);
},
updateStepsIndicator: function (current) {
var steps = ['input', 'criteria', 'match', 'interview'];
var currentIdx = steps.indexOf(current);
$$('.step').forEach(function (el) {
var stepName = el.getAttribute('data-step');
var stepIdx = steps.indexOf(stepName);
el.classList.remove('step-active', 'step-done');
if (stepIdx === currentIdx) el.classList.add('step-active');
else if (stepIdx < currentIdx) el.classList.add('step-done');
});
},
renderCriteriaTable: function (criteria) {
var tbody = $('#criteria-tbody');
var emptyMsg = $('#criteria-empty');
var tableWrap = $('.criteria-table-wrap');
if (!tbody) return;
criteriaEditState = criteria.map(function (c) {
return {
id: c.id,
label: c.label,
required: c.required,
weight: c.weight,
synonyms: (c.synonyms || []).join(', ')
};
});
if (criteria.length === 0) {
tbody.innerHTML = '';
if (tableWrap) tableWrap.hidden = true;
if (emptyMsg) emptyMsg.hidden = false;
return;
}
if (tableWrap) tableWrap.hidden = false;
if (emptyMsg) emptyMsg.hidden = true;
var html = '';
for (var i = 0; i < criteriaEditState.length; i++) {
var c = criteriaEditState[i];
html += '<tr data-id="' + c.id + '">';
html += '<td><input class="input-inline" type="text" value="' + escapeAttr(c.label) + '" data-field="label" aria-label="Название требования"></td>';
html += '<td><select class="select-inline" data-field="required" aria-label="Тип требования">';
html += '<option value="true"' + (c.required ? ' selected' : '') + '>Обязательное</option>';
html += '<option value="false"' + (!c.required ? ' selected' : '') + '>Желательное</option>';
html += '</select></td>';
html += '<td><select class="select-inline" data-field="weight" aria-label="Вес требования">';
for (var w = 1; w <= 5; w++) {
html += '<option value="' + w + '"' + (c.weight === w ? ' selected' : '') + '>' + w + '</option>';
}
html += '</select></td>';
html += '<td><input class="input-inline" type="text" value="' + escapeAttr(c.synonyms) + '" data-field="synonyms" aria-label="Синонимы через запятую" placeholder="синоним1, синоним2"></td>';
html += '<td><button class="btn-icon" data-action="delete-criterion" aria-label="Удалить требование «' + escapeAttr(c.label) + '»">';
html += '<svg width="16" height="16" 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 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
html += '</button></td>';
html += '</tr>';
}
tbody.innerHTML = html;
},
readCriteriaFromTable: function () {
var rows = $$('#criteria-tbody tr');
var criteria = [];
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var id = row.getAttribute('data-id');
var label = row.querySelector('[data-field="label"]').value.trim();
var required = row.querySelector('[data-field="required"]').value === 'true';
var weight = parseInt(row.querySelector('[data-field="weight"]').value, 10);
var synonymsStr = row.querySelector('[data-field="synonyms"]').value;
var synonyms = synonymsStr.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
criteria.push({ id: id, label: label, required: required, weight: weight, synonyms: synonyms });
}
return criteria;
},
renderMatchTable: function (criteria, matchResults, filter) {
var tbody = $('#match-tbody');
var emptyMsg = $('#match-empty');
var tableWrap = $('.match-table-wrap');
if (!tbody) return;
var filtered = APP.logic.getFilteredResults(matchResults, filter);
if (filtered.length === 0) {
tbody.innerHTML = '';
if (tableWrap) tableWrap.hidden = true;
if (emptyMsg) emptyMsg.hidden = false;
return;
}
if (tableWrap) tableWrap.hidden = false;
if (emptyMsg) emptyMsg.hidden = true;
var html = '';
for (var i = 0; i < filtered.length; i++) {
var r = filtered[i];
var c = APP.logic.getCriterionById(criteria, r.criterionId);
if (!c) continue;
var statusClass = 'status-' + r.status.replace('_', '-');
var statusIcon = '';
var statusText = '';
if (r.status === APP.STATUS.CONFIRMED) {
statusIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
statusText = 'Подтверждено';
} else if (r.status === APP.STATUS.NOT_FOUND) {
statusIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>';
statusText = 'Не найдено';
} else {
statusIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
statusText = 'Уточнить';
}
var evidenceHtml = '';
if (r.evidence && r.evidence.length > 0) {
evidenceHtml = '<ul class="evidence-list">';
for (var e = 0; e < r.evidence.length; e++) {
evidenceHtml += '<li class="evidence-fragment">' + escapeHtml(r.evidence[e].fragment) + '</li>';
}
evidenceHtml += '</ul>';
} else {
evidenceHtml = '<span style="color:var(--color-text-secondary);font-size:var(--text-xs)">Фрагменты не найдены</span>';
}
html += '<tr data-criterion-id="' + r.criterionId + '">';
html += '<td>' + escapeHtml(c.label) + '</td>';
html += '<td>' + evidenceHtml + '</td>';
html += '<td>';
html += '<select class="status-select" data-action="change-status" aria-label="Статус для «' + escapeAttr(c.label) + '»">';
html += '<option value="' + APP.STATUS.CONFIRMED + '"' + (r.status === APP.STATUS.CONFIRMED ? ' selected' : '') + '>Подтверждено</option>';
html += '<option value="' + APP.STATUS.NOT_FOUND + '"' + (r.status === APP.STATUS.NOT_FOUND ? ' selected' : '') + '>Не найдено</option>';
html += '<option value="' + APP.STATUS.UNCLEAR + '"' + (r.status === APP.STATUS.UNCLEAR ? ' selected' : '') + '>Уточнить</option>';
html += '</select>';
html += '</td>';
html += '<td><input class="note-input" type="text" value="' + escapeAttr(r.note || '') + '" data-action="change-note" placeholder="Примечание…" aria-label="Примечание для «' + escapeAttr(c.label) + '»"></td>';
html += '</tr>';
}
tbody.innerHTML = html;
},
renderCoverage: function (coverage) {
var bar = $('#coverage-bar-fill');
var value = $('#coverage-value');
if (bar) {
bar.style.width = coverage + '%';
bar.setAttribute('aria-valuenow', coverage);
}
if (value) {
value.textContent = coverage + '%';
}
},
renderInterview: function (interviewData) {
var container = $('#interview-questions');
var emptyMsg = $('#interview-empty');
if (!container) return;
if (interviewData.length === 0) {
container.innerHTML = '';
if (emptyMsg) emptyMsg.hidden = false;
return;
}
if (emptyMsg) emptyMsg.hidden = true;
var statusLabels = {};
statusLabels[APP.STATUS.NOT_FOUND] = 'Не найдено';
statusLabels[APP.STATUS.UNCLEAR] = 'Требует уточнения';
var html = '';
var globalNum = 1;
for (var i = 0; i < interviewData.length; i++) {
var section = interviewData[i];
html += '<div class="interview-section">';
html += '<div class="interview-section-title">' + escapeHtml(section.criterion.label);
html += ' <span class="status-badge status-' + section.status.replace('_', '-') + '">' + (statusLabels[section.status] || section.status) + '</span>';
html += '</div>';
html += '<div class="interview-card">';
html += '<div class="interview-card-criterion">';
if (section.criterion.required) html += 'Обязательное';
else html += 'Желательное';
html += ' · Вес ' + section.criterion.weight;
html += '</div>';
html += '<ol class="interview-questions-list">';
for (var q = 0; q < section.questions.length; q++) {
html += '<li class="interview-question"><span class="interview-question-num">' + globalNum + '.</span> ' + escapeHtml(section.questions[q]) + '</li>';
globalNum++;
}
html += '</ol>';
html += '</div>';
html += '</div>';
}
container.innerHTML = html;
},
updateFilterChips: function (activeFilter) {
$$('.chip').forEach(function (chip) {
var f = chip.getAttribute('data-filter');
if (f === activeFilter) {
chip.classList.add('chip-active');
} else {
chip.classList.remove('chip-active');
}
});
}
};
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function escapeAttr(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
})();