246 lines
7.0 KiB
JavaScript
246 lines
7.0 KiB
JavaScript
/**
|
||
* Matcher - логика сопоставления резюме и вакансии
|
||
*/
|
||
|
||
function extractRequirements(vacancyText) {
|
||
if (!vacancyText || typeof vacancyText !== 'string') {
|
||
return { criteria: [], error: 'Введите текст вакансии' };
|
||
}
|
||
|
||
const result = window.aiAdapter.generate('extractCriteria', { vacancyText });
|
||
return result;
|
||
}
|
||
|
||
function matchResumeToCriteria(criteria, resumeText) {
|
||
if (!criteria || criteria.length === 0) {
|
||
return { results: [], coverage: 0, error: 'Список критериев пуст' };
|
||
}
|
||
|
||
if (!resumeText || typeof resumeText !== 'string') {
|
||
return { results: [], coverage: 0, error: 'Введите текст резюме' };
|
||
}
|
||
|
||
const result = window.aiAdapter.generate('matchResume', { criteria, resumeText });
|
||
return result;
|
||
}
|
||
|
||
function updateCriterionStatus(results, criterionId, newStatus, note) {
|
||
if (!results || !Array.isArray(results)) {
|
||
return results;
|
||
}
|
||
|
||
return results.map(result => {
|
||
if (result.criterionId === criterionId) {
|
||
return {
|
||
...result,
|
||
status: newStatus,
|
||
note: note || result.note
|
||
};
|
||
}
|
||
return result;
|
||
});
|
||
}
|
||
|
||
function addCustomCriterion(criteria, label, required, weight, synonyms) {
|
||
const newCriterion = {
|
||
id: `custom-${Date.now()}`,
|
||
label: label.trim(),
|
||
required: required !== false,
|
||
weight: Math.max(1, Math.min(5, parseInt(weight, 10) || 3)),
|
||
synonyms: (synonyms || []).filter(s => s.trim().length > 0),
|
||
category: 'custom'
|
||
};
|
||
|
||
return [...criteria, newCriterion];
|
||
}
|
||
|
||
function removeCriterion(criteria, criterionId) {
|
||
return criteria.filter(c => c.id !== criterionId);
|
||
}
|
||
|
||
function updateCriterion(criteria, criterionId, updates) {
|
||
return criteria.map(c => {
|
||
if (c.id === criterionId) {
|
||
return { ...c, ...updates };
|
||
}
|
||
return c;
|
||
});
|
||
}
|
||
|
||
function generateInterviewQuestions(matchResults) {
|
||
if (!matchResults || !matchResults.results || matchResults.results.length === 0) {
|
||
return { questions: [] };
|
||
}
|
||
|
||
const unmatched = matchResults.results.filter(
|
||
r => r.status === 'clarify' || r.status === 'notfound'
|
||
);
|
||
|
||
if (unmatched.length === 0) {
|
||
return { questions: [] };
|
||
}
|
||
|
||
const result = window.aiAdapter.generate('generateQuestions', {
|
||
unmatchedCriteria: unmatched
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
function calculateCoverage(results) {
|
||
if (!results || results.length === 0) {
|
||
return 0;
|
||
}
|
||
|
||
const totalWeight = results.reduce((sum, r) => sum + (r.weight || 0), 0);
|
||
if (totalWeight === 0) {
|
||
return 0;
|
||
}
|
||
|
||
const confirmedWeight = results
|
||
.filter(r => r.status === 'confirmed')
|
||
.reduce((sum, r) => sum + (r.weight || 0), 0);
|
||
|
||
return Math.round((confirmedWeight / totalWeight) * 100);
|
||
}
|
||
|
||
function getStatusStats(results) {
|
||
if (!results || !Array.isArray(results)) {
|
||
return { confirmed: 0, clarify: 0, notfound: 0, total: 0 };
|
||
}
|
||
|
||
return {
|
||
confirmed: results.filter(r => r.status === 'confirmed').length,
|
||
clarify: results.filter(r => r.status === 'clarify').length,
|
||
notfound: results.filter(r => r.status === 'notfound').length,
|
||
total: results.length
|
||
};
|
||
}
|
||
|
||
function exportSummary(vacancyText, resumeText, criteria, matchResults, questions) {
|
||
const coverage = calculateCoverage(matchResults?.results || []);
|
||
const stats = getStatusStats(matchResults?.results || []);
|
||
|
||
const summary = {
|
||
exportedAt: new Date().toISOString(),
|
||
vacancy: {
|
||
title: 'Вакансия',
|
||
criteriaCount: criteria?.length || 0
|
||
},
|
||
coverage: {
|
||
percentage: coverage,
|
||
confirmed: stats.confirmed,
|
||
toClarify: stats.clarify,
|
||
notFound: stats.notfound,
|
||
total: stats.total
|
||
},
|
||
results: matchResults?.results?.map(r => ({
|
||
requirement: r.label,
|
||
type: r.required ? 'Обязательное' : 'Желательное',
|
||
weight: r.weight,
|
||
status: getStatusLabel(r.status),
|
||
evidence: r.evidence?.[0] || 'Нет данных'
|
||
})) || [],
|
||
interviewQuestions: questions?.questions?.map(q => ({
|
||
topic: q.label,
|
||
question: q.question,
|
||
followUp: q.followUp
|
||
})) || []
|
||
};
|
||
|
||
return summary;
|
||
}
|
||
|
||
function getStatusLabel(status) {
|
||
const labels = {
|
||
confirmed: 'Подтверждено',
|
||
clarify: 'Уточнить',
|
||
notfound: 'Не найдено'
|
||
};
|
||
return labels[status] || status;
|
||
}
|
||
|
||
function downloadJSON(data, filename) {
|
||
const json = JSON.stringify(data, null, 2);
|
||
const blob = new Blob([json], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
function downloadText(text, filename) {
|
||
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
function formatExportText(summary) {
|
||
let text = 'HR CANDIDATE MATCHER — СВОДКА\n';
|
||
text += '=' .repeat(50) + '\n\n';
|
||
text += `Дата экспорта: ${new Date(summary.exportedAt).toLocaleString('ru-RU')}\n\n`;
|
||
|
||
text += 'ПОКРЫТИЕ ТРЕБОВАНИЙ\n';
|
||
text += '-'.repeat(30) + '\n';
|
||
text += `Общее покрытие: ${summary.coverage.percentage}%\n`;
|
||
text += `Подтверждено: ${summary.coverage.confirmed}\n`;
|
||
text += `Требует уточнения: ${summary.coverage.toClarify}\n`;
|
||
text += `Не найдено: ${summary.coverage.notFound}\n`;
|
||
text += `Всего критериев: ${summary.coverage.total}\n\n`;
|
||
|
||
text += 'РЕЗУЛЬТАТЫ СОПОСТАВЛЕНИЯ\n';
|
||
text += '-'.repeat(30) + '\n';
|
||
summary.results.forEach((r, i) => {
|
||
text += `${i + 1}. ${r.requirement}\n`;
|
||
text += ` Тип: ${r.type}, Вес: ${r.weight}\n`;
|
||
text += ` Статус: ${r.status}\n`;
|
||
text += ` Доказательство: ${r.evidence}\n\n`;
|
||
});
|
||
|
||
if (summary.interviewQuestions.length > 0) {
|
||
text += 'ВОПРОСЫ ДЛЯ ИНТЕРВЬЮ\n';
|
||
text += '-'.repeat(30) + '\n';
|
||
summary.interviewQuestions.forEach((q, i) => {
|
||
text += `${i + 1}. Тема: ${q.topic}\n`;
|
||
text += ` Вопрос: ${q.question}\n`;
|
||
if (q.followUp && q.followUp.length > 0) {
|
||
text += ` Дополнительные: ${q.followUp.join('; ')}\n`;
|
||
}
|
||
text += '\n';
|
||
});
|
||
}
|
||
|
||
text += '=' .repeat(50) + '\n';
|
||
text += 'Создано с помощью HR Candidate Matcher\n';
|
||
|
||
return text;
|
||
}
|
||
|
||
if (typeof module !== 'undefined' && module.exports) {
|
||
module.exports = {
|
||
extractRequirements,
|
||
matchResumeToCriteria,
|
||
updateCriterionStatus,
|
||
addCustomCriterion,
|
||
removeCriterion,
|
||
updateCriterion,
|
||
generateInterviewQuestions,
|
||
calculateCoverage,
|
||
getStatusStats,
|
||
exportSummary,
|
||
downloadJSON,
|
||
downloadText,
|
||
formatExportText
|
||
};
|
||
}
|