166 lines
5.6 KiB
JavaScript
166 lines
5.6 KiB
JavaScript
/* HR Candidate Matcher — Business Logic */
|
||
/* global window */
|
||
(function () {
|
||
'use strict';
|
||
|
||
var APP = window.HRMatcher = window.HRMatcher || {};
|
||
|
||
APP.logic = {
|
||
validateInput: function (vacancyText, resumeText) {
|
||
var errors = {};
|
||
var valid = true;
|
||
|
||
if (!vacancyText || vacancyText.trim().length < 20) {
|
||
errors.vacancy = 'Введите текст вакансии (минимум 20 символов)';
|
||
valid = false;
|
||
}
|
||
if (!resumeText || resumeText.trim().length < 20) {
|
||
errors.resume = 'Введите текст резюме (минимум 20 символов)';
|
||
valid = false;
|
||
}
|
||
|
||
return { valid: valid, errors: errors };
|
||
},
|
||
|
||
validateCriteria: function (criteria) {
|
||
if (!criteria || criteria.length === 0) {
|
||
return { valid: false, message: 'Добавьте хотя бы один критерий' };
|
||
}
|
||
for (var i = 0; i < criteria.length; i++) {
|
||
var c = criteria[i];
|
||
if (!c.label || c.label.trim().length < 2) {
|
||
return { valid: false, message: 'Укажите название для критерия №' + (i + 1) };
|
||
}
|
||
if (c.weight < 1 || c.weight > 5 || isNaN(c.weight)) {
|
||
return { valid: false, message: 'Вес критерия «' + c.label + '» должен быть от 1 до 5' };
|
||
}
|
||
}
|
||
return { valid: true };
|
||
},
|
||
|
||
runMatch: function (criteria, resumeText) {
|
||
var results = [];
|
||
for (var i = 0; i < criteria.length; i++) {
|
||
var result = APP.aiAdapter.matchCriterion(criteria[i], resumeText);
|
||
results.push(result);
|
||
}
|
||
return results;
|
||
},
|
||
|
||
getFilteredResults: function (matchResults, filter) {
|
||
if (!filter || filter === 'all') return matchResults;
|
||
return matchResults.filter(function (r) { return r.status === filter; });
|
||
},
|
||
|
||
getCriterionById: function (criteria, id) {
|
||
for (var i = 0; i < criteria.length; i++) {
|
||
if (criteria[i].id === id) return criteria[i];
|
||
}
|
||
return null;
|
||
},
|
||
|
||
getUnconfirmedCriteria: function (criteria, matchResults) {
|
||
var resultMap = {};
|
||
matchResults.forEach(function (r) { resultMap[r.criterionId] = r; });
|
||
return criteria.filter(function (c) {
|
||
var r = resultMap[c.id];
|
||
return !r || r.status !== APP.STATUS.CONFIRMED;
|
||
});
|
||
},
|
||
|
||
generateInterviewData: function (criteria, matchResults) {
|
||
var resultMap = {};
|
||
matchResults.forEach(function (r) { resultMap[r.criterionId] = r; });
|
||
|
||
var sections = [];
|
||
var unconfirmed = this.getUnconfirmedCriteria(criteria, matchResults);
|
||
|
||
unconfirmed.forEach(function (c) {
|
||
var result = resultMap[c.id];
|
||
var status = result ? result.status : APP.STATUS.NOT_FOUND;
|
||
var questions = APP.aiAdapter.generateQuestions(c, result);
|
||
sections.push({
|
||
criterion: c,
|
||
status: status,
|
||
questions: questions
|
||
});
|
||
});
|
||
|
||
return sections;
|
||
},
|
||
|
||
exportSummary: function (vacancy, criteria, matchResults) {
|
||
var lines = [];
|
||
var now = new Date();
|
||
lines.push('=== Сводка по кандидату ===');
|
||
lines.push('Дата: ' + now.toLocaleDateString('ru-KZ'));
|
||
lines.push('Позиция: ' + (vacancy.title || 'Не указана'));
|
||
lines.push('');
|
||
|
||
var coverage = APP.aiAdapter.computeCoverage(criteria, matchResults);
|
||
lines.push('Покрытие указанных требований: ' + coverage + '%');
|
||
lines.push('');
|
||
|
||
var resultMap = {};
|
||
matchResults.forEach(function (r) { resultMap[r.criterionId] = r; });
|
||
|
||
var statusLabels = {};
|
||
statusLabels[APP.STATUS.CONFIRMED] = 'Подтверждено';
|
||
statusLabels[APP.STATUS.NOT_FOUND] = 'Не найдено';
|
||
statusLabels[APP.STATUS.UNCLEAR] = 'Уточнить';
|
||
|
||
lines.push('--- Критерии ---');
|
||
criteria.forEach(function (c) {
|
||
var result = resultMap[c.id];
|
||
var statusText = result ? statusLabels[result.status] || '—' : '—';
|
||
var typeText = c.required ? 'Обязательное' : 'Желательное';
|
||
lines.push('• ' + c.label + ' [' + typeText + ', вес ' + c.weight + '] — ' + statusText);
|
||
if (result && result.note) {
|
||
lines.push(' Примечание: ' + result.note);
|
||
}
|
||
});
|
||
|
||
lines.push('');
|
||
lines.push('--- Вопросы для интервью ---');
|
||
var interviewData = this.generateInterviewData(criteria, matchResults);
|
||
var qNum = 1;
|
||
interviewData.forEach(function (section) {
|
||
lines.push('');
|
||
lines.push('▸ ' + section.criterion.label + ' (' + (statusLabels[section.status] || '—') + ')');
|
||
section.questions.forEach(function (q) {
|
||
lines.push(' ' + qNum + '. ' + q);
|
||
qNum++;
|
||
});
|
||
});
|
||
|
||
lines.push('');
|
||
lines.push('---');
|
||
lines.push('Данные обезличены. Сводка подготовлена в демонстрационном режиме.');
|
||
lines.push('Персональные данные кандидата не включены.');
|
||
|
||
return lines.join('\n');
|
||
},
|
||
|
||
formatNumber: function (num) {
|
||
try {
|
||
return new Intl.NumberFormat('ru-KZ').format(num);
|
||
} catch (e) {
|
||
return String(num);
|
||
}
|
||
},
|
||
|
||
formatCurrency: function (amount) {
|
||
try {
|
||
return new Intl.NumberFormat('ru-KZ', {
|
||
style: 'currency',
|
||
currency: 'KZT',
|
||
minimumFractionDigits: 0,
|
||
maximumFractionDigits: 0
|
||
}).format(amount);
|
||
} catch (e) {
|
||
return amount + ' ₸';
|
||
}
|
||
}
|
||
};
|
||
})();
|