/* ============================================================ matching.jsx — подбор вакансий под анкету человека. Чистые функции: профиль + вакансия → процент совпадения, список «почему подходит» и «чего не хватает». Демо-макет: критерии вакансий заданы в i18n.jsx (JOBS[].crit). ============================================================ */ /* Пустой профиль. Заполняется онбордингом и резюме-интервью. */ const EMPTY_PROFILE = { name: null, // 'Aziz Rahimov' gender: null, // 'm' | 'f' age: null, // число city: null, // { ru, uz } dest: null, // ключ страны или 'any' prof: null, // 'weld' | 'build' | 'driver' | 'ware' | 'care' | 'other' profTitle: null, // { ru, uz, en } — как называется должность exp: null, // лет опыта, число places: [], // [{ company, role, years, duties: [] }] skills: [], // [{ ru, uz, en }] certs: [], // [{ ru, uz, en }] langs: [], // [{ code, level }] level: 'a1'|'a2'|'b1'|'b2'|'c1'|'native' passport: null, // 'yes' | 'old' | 'no' photo: false, // загрузил ли фото budget: null, // 'lo' | 'mid' | 'hi' | 'credit' }; const LANG_RANK = { none: 0, a1: 1, a2: 2, b1: 3, b2: 4, c1: 5, native: 6 }; function langLevel(profile, code) { const l = (profile.langs || []).find(x => x.code === code); return l ? (LANG_RANK[l.level] || 0) : 0; } /* Веса. Сумма = 100. Профессия и опыт решают, остальное корректирует. */ const W = { prof: 34, exp: 20, age: 14, gender: 8, lang: 14, docs: 10 }; /* scoreJob(profile, job) → { score, pros, gaps, blocked } pros / gaps — массивы { ru, uz } для показа в карточке. blocked = true, если человек формально не проходит (возраст, пол): такие вакансии не прячем, но честно пишем причину. */ function scoreJob(profile, job) { const c = job.crit || {}; const pros = []; const gaps = []; let score = 0; let blocked = false; // --- профессия --- if (c.profs && profile.prof) { if (c.profs.includes(profile.prof)) { score += W.prof; pros.push({ ru: 'Ваша профессия — прямое попадание в требования', uz: 'Kasbingiz talablarga to‘liq mos', }); } else if (c.noExp) { score += Math.round(W.prof * 0.55); pros.push({ ru: 'Берут без опыта в этой профессии — вас рассмотрят', uz: 'Bu kasbda tajribasiz olishadi — sizni ko‘rib chiqishadi', }); } else { gaps.push({ ru: 'Профессия другая — работодатель попросит переучиться', uz: 'Kasb boshqa — ish beruvchi qayta o‘qishni so‘raydi', }); } } else { score += Math.round(W.prof * 0.5); } // --- опыт --- const need = c.expMin || 0; const have = profile.exp == null ? 0 : profile.exp; if (have >= need) { score += W.exp; if (need > 0) { pros.push({ ru: `Опыта ${have} ${plYears(have, 'ru')} — требуется от ${need}`, uz: `${have} yil tajriba — kamida ${need} yil talab qilinadi`, }); } } else { score += Math.round(W.exp * (have / Math.max(need, 1)) * 0.6); gaps.push({ ru: `Нужен опыт от ${need} ${plYears(need, 'ru')}, у вас ${have}`, uz: `Kamida ${need} yil tajriba kerak, sizda ${have}`, }); } // --- возраст --- if (profile.age != null && (c.ageMin || c.ageMax)) { const lo = c.ageMin || 0, hi = c.ageMax || 99; if (profile.age >= lo && profile.age <= hi) { score += W.age; } else { blocked = true; gaps.push({ ru: `Возраст по вакансии ${lo}–${hi}, вам ${profile.age}`, uz: `Vakansiya bo‘yicha yosh ${lo}–${hi}, sizda ${profile.age}`, }); } } else { score += W.age; } // --- пол --- if (c.gender && c.gender !== 'any' && profile.gender) { if (c.gender === profile.gender) { score += W.gender; } else { blocked = true; gaps.push({ ru: c.gender === 'f' ? 'Работодатель набирает только женщин' : 'Работодатель набирает только мужчин', uz: c.gender === 'f' ? 'Ish beruvchi faqat ayollarni oladi' : 'Ish beruvchi faqat erkaklarni oladi', }); } } else { score += W.gender; } // --- язык --- if (c.lang) { const has = langLevel(profile, c.lang.code); const req = LANG_RANK[c.lang.level] || 0; if (has >= req) { score += W.lang; pros.push({ ru: `Языка хватает: ${LANG_NAMES[c.lang.code].ru} ${c.lang.level.toUpperCase()}`, uz: `Til yetarli: ${LANG_NAMES[c.lang.code].uz} ${c.lang.level.toUpperCase()}`, }); } else { gaps.push({ ru: `Нужен ${LANG_NAMES[c.lang.code].ru} ${c.lang.level.toUpperCase()} — это курс и экзамен`, uz: `${LANG_NAMES[c.lang.code].uz} ${c.lang.level.toUpperCase()} kerak — kurs va imtihon`, }); } } else { score += W.lang; pros.push({ ru: 'Язык не требуют', uz: 'Til talab qilinmaydi' }); } // --- документы --- if (profile.passport === 'yes') { score += W.docs; } else if (profile.passport === 'old') { score += Math.round(W.docs * 0.5); gaps.push({ ru: 'Загранпаспорт скоро истекает — меняйте сейчас', uz: 'Xorijiy pasport muddati tugayapti — hozir almashtiring', }); } else if (profile.passport === 'no') { gaps.push({ ru: 'Нет загранпаспорта — это 2–4 недели', uz: 'Xorijiy pasport yo‘q — bu 2–4 hafta', }); } else { score += Math.round(W.docs * 0.5); } score = Math.max(4, Math.min(99, Math.round(score))); if (blocked) score = Math.min(score, 45); return { score, pros: pros.slice(0, 3), gaps: gaps.slice(0, 3), blocked }; } const LANG_NAMES = { ru: { ru: 'русский', uz: 'rus tili' }, en: { ru: 'английский', uz: 'ingliz tili' }, ko: { ru: 'корейский', uz: 'koreys tili' }, de: { ru: 'немецкий', uz: 'nemis tili' }, pl: { ru: 'польский', uz: 'polyak tili' }, ja: { ru: 'японский', uz: 'yapon tili' }, }; function plYears(n, lang) { if (lang !== 'ru') return 'yil'; const a = n % 10, b = n % 100; if (a === 1 && b !== 11) return 'год'; if (a >= 2 && a <= 4 && (b < 12 || b > 14)) return 'года'; return 'лет'; } /* Склонение слова «отзыв» для русского; в узбекском форма одна. */ function nReviews(n, lang) { if (lang !== 'ru') return `${n} ta sharh`; const a = n % 10, b = n % 100; const w = (a === 1 && b !== 11) ? 'отзыв' : (a >= 2 && a <= 4 && (b < 12 || b > 14)) ? 'отзыва' : 'отзывов'; return `${n} ${w}`; } /* Есть ли в профиле хоть что-то, чтобы считать совпадение честно. */ function profileReady(profile) { return !!(profile && profile.prof && profile.exp != null); } /* Список вакансий, отсортированный по совпадению. */ function rankJobs(profile, jobs) { return jobs .map(j => ({ job: j, ...(profileReady(profile) ? scoreJob(profile, j) : { score: j.match, pros: [], gaps: [], blocked: false }) })) .sort((a, b) => b.score - a.score); } Object.assign(window, { EMPTY_PROFILE, scoreJob, rankJobs, profileReady, LANG_NAMES, LANG_RANK, plYears, nReviews });