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

435 lines
15 KiB
JavaScript
Raw Permalink 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 — 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();
}
})();