435 lines
15 KiB
JavaScript
435 lines
15 KiB
JavaScript
/* HR Candidate Matcher — Application Entry Point */
|
||
/* global window, document, HRMatcher */
|
||
(function () {
|
||
'use strict';
|
||
|
||
var APP = window.HRMatcher;
|
||
var $ = function (sel) { return document.querySelector(sel); };
|
||
var $$ = function (sel) { return Array.prototype.slice.call(document.querySelectorAll(sel)); };
|
||
|
||
var state = APP.createAppState();
|
||
|
||
function saveState() {
|
||
var data = {
|
||
currentScreen: state.currentScreen,
|
||
vacancy: state.vacancy,
|
||
resumeText: state.isDemo ? '' : state.resumeText,
|
||
matchResults: state.matchResults,
|
||
isDemo: state.isDemo
|
||
};
|
||
APP.storage.save(data);
|
||
}
|
||
|
||
function loadState() {
|
||
var data = APP.storage.load();
|
||
if (!data) return false;
|
||
state.currentScreen = data.currentScreen || 'welcome';
|
||
state.vacancy = data.vacancy || null;
|
||
state.resumeText = data.resumeText || '';
|
||
state.matchResults = data.matchResults || [];
|
||
state.isDemo = data.isDemo || false;
|
||
return true;
|
||
}
|
||
|
||
function navigateTo(screen) {
|
||
state.currentScreen = screen;
|
||
APP.ui.showScreen(screen);
|
||
|
||
if (screen === 'input') {
|
||
var vacInput = $('#input-vacancy');
|
||
var resInput = $('#input-resume');
|
||
if (vacInput && state.vacancy) vacInput.value = state.vacancy.rawText || '';
|
||
if (resInput) resInput.value = state.resumeText || '';
|
||
} else if (screen === 'criteria') {
|
||
if (state.vacancy) {
|
||
APP.ui.renderCriteriaTable(state.vacancy.criteria || []);
|
||
}
|
||
} else if (screen === 'match') {
|
||
if (state.vacancy) {
|
||
currentFilter = 'all';
|
||
APP.ui.updateFilterChips('all');
|
||
APP.ui.renderMatchTable(state.vacancy.criteria, state.matchResults, 'all');
|
||
APP.ui.renderCoverage(APP.aiAdapter.computeCoverage(state.vacancy.criteria, state.matchResults));
|
||
}
|
||
} else if (screen === 'interview') {
|
||
if (state.vacancy) {
|
||
var interviewData = APP.logic.generateInterviewData(state.vacancy.criteria, state.matchResults);
|
||
APP.ui.renderInterview(interviewData);
|
||
}
|
||
}
|
||
|
||
saveState();
|
||
}
|
||
|
||
var currentFilter = 'all';
|
||
|
||
function bindEvents() {
|
||
var btnStartEmpty = $('#btn-start-empty');
|
||
var btnStartDemo = $('#btn-start-demo');
|
||
var btnBackInput = $('#btn-back-from-input');
|
||
var btnBackCriteria = $('#btn-back-from-criteria');
|
||
var btnBackMatch = $('#btn-back-from-match');
|
||
var btnBackInterview = $('#btn-back-from-interview');
|
||
var formInput = $('#form-input');
|
||
var btnAddCriterion = $('#btn-add-criterion');
|
||
var btnToMatch = $('#btn-to-match');
|
||
var btnToInterview = $('#btn-to-interview');
|
||
var btnViewSource = $('#btn-view-source');
|
||
var btnCloseSource = $('#btn-close-source');
|
||
var btnCopyQuestions = $('#btn-copy-questions');
|
||
var btnExportSummary = $('#btn-export-summary');
|
||
var btnStartOver = $('#btn-start-over');
|
||
var btnClearAll = $('#btn-clear-all');
|
||
var btnConfirmOk = $('#btn-confirm-ok');
|
||
var btnConfirmCancel = $('#btn-confirm-cancel');
|
||
var btnResetFilter = $('#btn-reset-filter');
|
||
|
||
if (btnStartEmpty) {
|
||
btnStartEmpty.addEventListener('click', function () {
|
||
state = APP.createAppState();
|
||
state.currentScreen = 'input';
|
||
navigateTo('input');
|
||
});
|
||
}
|
||
|
||
if (btnStartDemo) {
|
||
btnStartDemo.addEventListener('click', function () {
|
||
state = APP.createAppState();
|
||
state.isDemo = true;
|
||
state.vacancy = APP.createVacancy('Таргетолог', APP.DEMO_VACANCY_TEXT, []);
|
||
state.resumeText = APP.DEMO_RESUME_TEXT;
|
||
state.vacancy.criteria = APP.aiAdapter.extractRequirements(APP.DEMO_VACANCY_TEXT);
|
||
state.matchResults = APP.logic.runMatch(state.vacancy.criteria, state.resumeText);
|
||
state.currentScreen = 'criteria';
|
||
navigateTo('criteria');
|
||
APP.ui.showToast('Загружен пример. Проверьте критерии.', 'success');
|
||
});
|
||
}
|
||
|
||
if (btnBackInput) btnBackInput.addEventListener('click', function () { navigateTo('welcome'); });
|
||
if (btnBackCriteria) btnBackCriteria.addEventListener('click', function () { navigateTo('input'); });
|
||
if (btnBackMatch) btnBackMatch.addEventListener('click', function () { navigateTo('criteria'); });
|
||
if (btnBackInterview) btnBackInterview.addEventListener('click', function () { navigateTo('match'); });
|
||
|
||
if (formInput) {
|
||
formInput.addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
APP.ui.clearAllErrors();
|
||
|
||
var vacancyText = $('#input-vacancy').value;
|
||
var resumeText = $('#input-resume').value;
|
||
var validation = APP.logic.validateInput(vacancyText, resumeText);
|
||
|
||
if (!validation.valid) {
|
||
if (validation.errors.vacancy) {
|
||
APP.ui.showError('vacancy', validation.errors.vacancy);
|
||
$('#input-vacancy').focus();
|
||
} else if (validation.errors.resume) {
|
||
APP.ui.showError('resume', validation.errors.resume);
|
||
$('#input-resume').focus();
|
||
}
|
||
return;
|
||
}
|
||
|
||
var title = vacancyText.split('\n')[0].substring(0, 80).trim();
|
||
state.vacancy = APP.createVacancy(title, vacancyText, []);
|
||
state.resumeText = resumeText;
|
||
state.isDemo = false;
|
||
state.vacancy.criteria = APP.aiAdapter.extractRequirements(vacancyText);
|
||
state.matchResults = APP.logic.runMatch(state.vacancy.criteria, resumeText);
|
||
|
||
navigateTo('criteria');
|
||
APP.ui.showToast('Извлечено требований: ' + state.vacancy.criteria.length, 'success');
|
||
});
|
||
}
|
||
|
||
if (btnAddCriterion) {
|
||
btnAddCriterion.addEventListener('click', function () {
|
||
var criteria = APP.ui.readCriteriaFromTable();
|
||
criteria.push(APP.createCriterion('', true, 3, []));
|
||
state.vacancy.criteria = criteria;
|
||
APP.ui.renderCriteriaTable(criteria);
|
||
var rows = $$('#criteria-tbody tr');
|
||
var lastRow = rows[rows.length - 1];
|
||
if (lastRow) {
|
||
var input = lastRow.querySelector('[data-field="label"]');
|
||
if (input) input.focus();
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnToMatch) {
|
||
btnToMatch.addEventListener('click', function () {
|
||
var criteria = APP.ui.readCriteriaFromTable();
|
||
var validation = APP.logic.validateCriteria(criteria);
|
||
if (!validation.valid) {
|
||
APP.ui.showToast(validation.message, 'error');
|
||
return;
|
||
}
|
||
state.vacancy.criteria = criteria;
|
||
state.matchResults = APP.logic.runMatch(criteria, state.resumeText);
|
||
navigateTo('match');
|
||
});
|
||
}
|
||
|
||
var criteriaTbody = $('#criteria-tbody');
|
||
if (criteriaTbody) {
|
||
criteriaTbody.addEventListener('click', function (e) {
|
||
var btn = e.target.closest('[data-action="delete-criterion"]');
|
||
if (!btn) return;
|
||
var row = btn.closest('tr');
|
||
if (!row) return;
|
||
var id = row.getAttribute('data-id');
|
||
var criteria = APP.ui.readCriteriaFromTable();
|
||
var label = '';
|
||
for (var i = 0; i < criteria.length; i++) {
|
||
if (criteria[i].id === id) { label = criteria[i].label; break; }
|
||
}
|
||
APP.ui.showConfirm('Удалить требование' + (label ? ' «' + label + '»' : '') + '?', function () {
|
||
var updated = criteria.filter(function (c) { return c.id !== id; });
|
||
state.vacancy.criteria = updated;
|
||
APP.ui.renderCriteriaTable(updated);
|
||
APP.ui.hideConfirm();
|
||
});
|
||
});
|
||
}
|
||
|
||
var matchTbody = $('#match-tbody');
|
||
if (matchTbody) {
|
||
matchTbody.addEventListener('change', function (e) {
|
||
var target = e.target;
|
||
var row = target.closest('tr');
|
||
if (!row) return;
|
||
var criterionId = row.getAttribute('data-criterion-id');
|
||
|
||
if (target.getAttribute('data-action') === 'change-status') {
|
||
for (var i = 0; i < state.matchResults.length; i++) {
|
||
if (state.matchResults[i].criterionId === criterionId) {
|
||
state.matchResults[i].status = target.value;
|
||
break;
|
||
}
|
||
}
|
||
APP.ui.renderCoverage(APP.aiAdapter.computeCoverage(state.vacancy.criteria, state.matchResults));
|
||
saveState();
|
||
}
|
||
});
|
||
|
||
matchTbody.addEventListener('input', function (e) {
|
||
var target = e.target;
|
||
if (target.getAttribute('data-action') !== 'change-note') return;
|
||
var row = target.closest('tr');
|
||
if (!row) return;
|
||
var criterionId = row.getAttribute('data-criterion-id');
|
||
for (var i = 0; i < state.matchResults.length; i++) {
|
||
if (state.matchResults[i].criterionId === criterionId) {
|
||
state.matchResults[i].note = target.value;
|
||
break;
|
||
}
|
||
}
|
||
saveState();
|
||
});
|
||
}
|
||
|
||
var filterBar = $('#match-filter-bar');
|
||
if (filterBar) {
|
||
filterBar.addEventListener('click', function (e) {
|
||
var chip = e.target.closest('.chip');
|
||
if (!chip) return;
|
||
currentFilter = chip.getAttribute('data-filter') || 'all';
|
||
APP.ui.updateFilterChips(currentFilter);
|
||
APP.ui.renderMatchTable(state.vacancy.criteria, state.matchResults, currentFilter);
|
||
});
|
||
}
|
||
|
||
if (btnResetFilter) {
|
||
btnResetFilter.addEventListener('click', function () {
|
||
currentFilter = 'all';
|
||
APP.ui.updateFilterChips('all');
|
||
APP.ui.renderMatchTable(state.vacancy.criteria, state.matchResults, 'all');
|
||
});
|
||
}
|
||
|
||
if (btnToInterview) {
|
||
btnToInterview.addEventListener('click', function () {
|
||
navigateTo('interview');
|
||
});
|
||
}
|
||
|
||
if (btnViewSource) {
|
||
btnViewSource.addEventListener('click', function () {
|
||
var modal = $('#modal-source');
|
||
var vacSource = $('#source-vacancy');
|
||
var resSource = $('#source-resume');
|
||
if (vacSource) vacSource.textContent = state.vacancy ? state.vacancy.rawText : '';
|
||
if (resSource) resSource.textContent = state.resumeText || '';
|
||
if (modal) modal.hidden = false;
|
||
});
|
||
}
|
||
|
||
if (btnCloseSource) {
|
||
btnCloseSource.addEventListener('click', function () {
|
||
var modal = $('#modal-source');
|
||
if (modal) modal.hidden = true;
|
||
});
|
||
}
|
||
|
||
if (btnCopyQuestions) {
|
||
btnCopyQuestions.addEventListener('click', function () {
|
||
var interviewData = APP.logic.generateInterviewData(state.vacancy.criteria, state.matchResults);
|
||
if (interviewData.length === 0) {
|
||
APP.ui.showToast('Нет вопросов для копирования', 'error');
|
||
return;
|
||
}
|
||
var text = '';
|
||
var num = 1;
|
||
interviewData.forEach(function (section) {
|
||
text += section.criterion.label + ':\n';
|
||
section.questions.forEach(function (q) {
|
||
text += num + '. ' + q + '\n';
|
||
num++;
|
||
});
|
||
text += '\n';
|
||
});
|
||
copyToClipboard(text.trim());
|
||
});
|
||
}
|
||
|
||
if (btnExportSummary) {
|
||
btnExportSummary.addEventListener('click', function () {
|
||
var summary = APP.logic.exportSummary(state.vacancy, state.vacancy.criteria, state.matchResults);
|
||
downloadFile('candidate-summary.txt', summary, 'text/plain;charset=utf-8');
|
||
APP.ui.showToast('Сводка экспортирована', 'success');
|
||
});
|
||
}
|
||
|
||
if (btnStartOver) {
|
||
btnStartOver.addEventListener('click', function () {
|
||
APP.ui.showConfirm('Начать заново? Текущие данные будут очищены.', function () {
|
||
state = APP.createAppState();
|
||
APP.storage.clear();
|
||
navigateTo('welcome');
|
||
APP.ui.hideConfirm();
|
||
});
|
||
});
|
||
}
|
||
|
||
if (btnClearAll) {
|
||
btnClearAll.addEventListener('click', function () {
|
||
APP.ui.showConfirm('Очистить все данные? Это действие нельзя отменить.', function () {
|
||
state = APP.createAppState();
|
||
APP.storage.clear();
|
||
navigateTo('welcome');
|
||
APP.ui.hideConfirm();
|
||
APP.ui.showToast('Все данные очищены', 'success');
|
||
});
|
||
});
|
||
}
|
||
|
||
if (btnConfirmOk) {
|
||
btnConfirmOk.addEventListener('click', function () {
|
||
APP.ui.resolveConfirm();
|
||
});
|
||
}
|
||
|
||
if (btnConfirmCancel) {
|
||
btnConfirmCancel.addEventListener('click', function () {
|
||
APP.ui.hideConfirm();
|
||
});
|
||
}
|
||
|
||
document.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Escape') {
|
||
var sourceModal = $('#modal-source');
|
||
var confirmModal = $('#modal-confirm');
|
||
if (sourceModal && !sourceModal.hidden) {
|
||
sourceModal.hidden = true;
|
||
} else if (confirmModal && !confirmModal.hidden) {
|
||
APP.ui.hideConfirm();
|
||
}
|
||
}
|
||
});
|
||
|
||
var sourceModal = $('#modal-source');
|
||
if (sourceModal) {
|
||
sourceModal.addEventListener('click', function (e) {
|
||
if (e.target === sourceModal) sourceModal.hidden = true;
|
||
});
|
||
}
|
||
var confirmModal = $('#modal-confirm');
|
||
if (confirmModal) {
|
||
confirmModal.addEventListener('click', function (e) {
|
||
if (e.target === confirmModal) APP.ui.hideConfirm();
|
||
});
|
||
}
|
||
|
||
window.addEventListener('beforeunload', function () {
|
||
if (state.currentScreen !== 'welcome' && !state.isDemo) {
|
||
saveState();
|
||
}
|
||
});
|
||
}
|
||
|
||
function copyToClipboard(text) {
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(function () {
|
||
APP.ui.showToast('Скопировано в буфер обмена', 'success');
|
||
}).catch(function () {
|
||
fallbackCopy(text);
|
||
});
|
||
} else {
|
||
fallbackCopy(text);
|
||
}
|
||
}
|
||
|
||
function fallbackCopy(text) {
|
||
var ta = document.createElement('textarea');
|
||
ta.value = text;
|
||
ta.style.position = 'fixed';
|
||
ta.style.left = '-9999px';
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
try {
|
||
document.execCommand('copy');
|
||
APP.ui.showToast('Скопировано в буфер обмена', 'success');
|
||
} catch (e) {
|
||
APP.ui.showToast('Не удалось скопировать. Выделите текст вручную.', 'error');
|
||
}
|
||
document.body.removeChild(ta);
|
||
}
|
||
|
||
function downloadFile(filename, content, mimeType) {
|
||
var blob = new Blob([content], { type: mimeType });
|
||
var url = URL.createObjectURL(blob);
|
||
var a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
a.style.display = 'none';
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
setTimeout(function () {
|
||
URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
}, 100);
|
||
}
|
||
|
||
function init() {
|
||
bindEvents();
|
||
|
||
var hasSaved = loadState();
|
||
if (hasSaved && state.currentScreen !== 'welcome') {
|
||
navigateTo(state.currentScreen);
|
||
if (state.isDemo) {
|
||
APP.ui.showToast('Загружены данные примера', 'success');
|
||
}
|
||
} else {
|
||
APP.ui.showScreen('welcome');
|
||
}
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|