/* 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 + ' ₸'; } } }; })();