const { jsPDF } = window.jspdf; let finalAvatarData = null; // Здесь будет лежать результат кропа let cropper = null; // Объект кроппера // 1. Инициализация при загрузке document.addEventListener('DOMContentLoaded', () => { setupNavigation(); setupExperience(); setupEducation(); setupCropper(); const grid = document.getElementById('soft-skills-grid'); const counter = document.getElementById('skill-count'); const LIMIT = 5; grid.addEventListener('change', () => { const checkedInputs = grid.querySelectorAll('input[name="soft_skill"]:checked'); const uncheckedInputs = grid.querySelectorAll('input[name="soft_skill"]:not(:checked)'); counter.innerText = checkedInputs.length; if (checkedInputs.length >= LIMIT) { // Если выбрали 5, отключаем остальные uncheckedInputs.forEach(input => input.disabled = true); } else { // Если меньше 5, включаем все назад grid.querySelectorAll('input[name="soft_skill"]').forEach(input => input.disabled = false); } }); }); // 2. Навигация между шагами function setupNavigation() { const steps = document.querySelectorAll('.form-step'); // Для всех кнопок "Далее" document.querySelectorAll('.next-btn').forEach((btn) => { btn.onclick = (e) => { e.preventDefault(); const currentStep = btn.closest('.form-step'); const nextStep = currentStep.nextElementSibling; if (nextStep && nextStep.classList.contains('form-step')) { currentStep.classList.remove('active'); nextStep.classList.add('active'); } }; }); // Для всех кнопок "Назад" document.querySelectorAll('.prev-btn').forEach((btn) => { btn.onclick = (e) => { e.preventDefault(); const currentStep = btn.closest('.form-step'); const prevStep = currentStep.previousElementSibling; if (prevStep && prevStep.classList.contains('form-step')) { currentStep.classList.remove('active'); prevStep.classList.add('active'); } }; }); } function setupExperience() { const addBtn = document.getElementById('addExperience'); const list = document.getElementById('experience-list'); if (addBtn) { addBtn.onclick = () => { // 1. Проверяем лимит (5 элементов) const currentItems = list.querySelectorAll('.experience-item').length; if (currentItems >= 5) { alert("Максимум 5 мест работы"); return; } const item = document.createElement('div'); item.className = 'experience-item'; item.innerHTML = `
`; // 2. Обработчик удаления с проверкой лимита для кнопки item.querySelector('.remove-btn').onclick = () => { item.remove(); // Показываем кнопку назад, если элементов стало меньше 5 if (list.querySelectorAll('.experience-item').length < 5) { addBtn.style.display = 'block'; } }; list.appendChild(item); // 3. Прячем кнопку, если достигли лимита if (list.querySelectorAll('.experience-item').length >= 5) { addBtn.style.display = 'none'; } }; } } function setupEducation() { const addBtn = document.getElementById('addEducation'); const list = document.getElementById('education-list'); if (addBtn) { addBtn.onclick = () => { // 1. Проверяем текущее количество добавленных учебных заведений const currentItems = list.querySelectorAll('.education-item').length; if (currentItems >= 5) { alert("Можно добавить не более 5 учебных заведений"); return; } const item = document.createElement('div'); item.className = 'education-item'; item.innerHTML = `
`; // 2. Обработчик удаления item.querySelector('.remove-btn').onclick = () => { item.remove(); // Если стало меньше 5, возвращаем кнопку добавления if (list.querySelectorAll('.education-item').length < 5) { addBtn.style.display = 'block'; } }; list.appendChild(item); // 3. Если достигли лимита, скрываем кнопку if (list.querySelectorAll('.education-item').length >= 5) { addBtn.style.display = 'none'; } }; } } function getProfessionalSkills() { const skills = {}; // Находим все родительские блоки навыков const skillItems = document.querySelectorAll('.skill-item'); skillItems.forEach(item => { const name = item.querySelector('.skill-name').innerText; // Находим внутри этого блока выбранную радиокнопку const checkedInput = item.querySelector('input[type="radio"]:checked'); skills[name] = checkedInput.value; }); return skills; } function getSoftSkills() { const softSkills = []; // Находим все выбранные чекбоксы в сетке soft-skills const checkedBoxes = document.querySelectorAll('#soft-skills-grid input[type="checkbox"]:checked'); checkedBoxes.forEach(box => { softSkills.push(box.value); }); return softSkills; } // --- ГЕНЕРАЦИЯ PDF --- document.getElementById("generateBtn").addEventListener("click", generatePDF); async function generatePDF() { const doc = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" }); const fonts = [ { url: "ProximaNova-Light.ttf", name: "Proxima-normal", style: "normal" }, { url: "ProximaNova-Regular.ttf", name: "Proxima-normal", style: "italic" }, // Лайфхак: читайте ниже { url: "ProximaNova-Bold.ttf", name: "Proxima-normal", style: "bold" }, ]; const backgroundUrl = "background.png"; try { await loadFont(doc, fonts); const background = await loadImage(backgroundUrl); doc.internal.events.subscribe('addPage', () => { doc.addImage(background, "PNG", 0, 0, 210, 297, undefined, "NONE"); }); doc.addImage(background, "PNG", 0, 0, 210, 297, undefined, "NONE"); const data = getFormData(); // Рендеринг в PDF (основано на вашем коде) drawName(doc, data.name, 110, 32); drawDolzh(doc, data.dolzh, 110, 50); drawZp(doc, data.zp, 170, 55); await drawAva(doc, data.avatar, 36, 22); drawCity(doc, data.city, 117, 60); drawTipZan(doc, data.tipZan, 110, 67); drawFormRab(doc, data.formRab, 110, 74); //let leftY = drawKontakt(doc, "КОНТАКТЫ", 20, 70); //leftY = drawTextLine(doc, `Дата рожд.: ${data.birthDate}`, 20, leftY); //leftY = drawTextLine(doc, `${data.phone}`, 20, leftY); //leftY = drawTextLine(doc, `Город: ${data.city}`, 20, leftY); generateTableLeft(doc, data); await drawCircles(doc, data); await generateTableAbout(doc, data.about, 110, 78, data); let rightY = 110; //rightY = drawParagraph(doc, `О себе: ${data.about}`, 95, rightY, 100); // Вывод опыта работы (простой список для примера) //таблицей будет // jobs тут массив с опытом работы doc.save("resume.pdf"); sendGS(data) } catch (error) { console.error("Ошибка:", error); alert("Ошибка при генерации. Проверьте консоль."); } } function phoneNumber(phone){ let output = ""; phone = phone.replace(/\D/g, ''); if(phone[0] == "7"){ output = phone.replace(/(\d{1})(\d{3})(\d{3})(\d{2})(\d{2})/, '+$1 $2 $3-$4-$5'); }//+7 999 999-99-99 else{ output = phone.replace(/(\d{1})(\d{3})(\d{3})(\d{2})(\d{2})/, '$1 $2 $3-$4-$5'); } return output } function smartWrap(text, limit) { if (!text || text.length <= limit) return text; const words = text.split(' '); let currentLine = ''; let result = []; words.forEach(word => { // Проверяем: если добавить слово, превысим ли мы лимит? if ((currentLine + word).length > limit) { result.push(currentLine.trim()); // Сохраняем готовую строку currentLine = word + ' '; // Начинаем новую строку с этого слова } else { currentLine += word + ' '; // Добавляем слово в текущую строку } }); result.push(currentLine.trim()); // Добавляем последний кусок return result.join('\n'); // Склеиваем всё через перенос строки } function generateTableLeft(doc, data){ body = [[{content: `КОНТАКТЫ`, styles: {fontSize: 14, textColor:['#ffffff'], cellPadding: {bottom: 4}}}]]; body.push([{content: `${phoneNumber(data.phone)}`, styles:{fontSize: 12, textColor:['#ffffff'], cellPadding:{bottom: 4, left: 6}}}]); body.push([{content: `${data.email}`, styles:{fontSize: 12, textColor:['#ffffff'], cellPadding:{bottom: 4, left: 6}}}]); body.push([{content: `${data.vk}`, styles:{fontSize: 12, textColor:['#ffffff'], cellPadding:{bottom: 6, left: 6}}}]); body.push([{content: `ПРОФЕССИОНАЛЬНЫЕ НАВЫКИ`, styles:{fontSize: 14, textColor:['#ffffff'], cellPadding:{top:2, bottom: 4}}}]); for(let i of Object.keys(data.proskills)){ body.push([{content: `${smartWrap(i, 18)}`, styles:{fontSize: 12, textColor:['#ffffff'], cellPadding:{bottom: 2}}}]); } body.push([{content: `ГИБКИЕ НАВЫКИ`, styles:{fontSize: 14, textColor:['#ffffff'], cellPadding:{top:6, bottom: 4}}}]); for(let i of data.softskills){ body.push([{content: `${i}`, styles:{fontSize: 12, textColor:['#ffffff'], cellPadding:{bottom: 2}}}]); } doc.autoTable({ startY: 70, margin: { left: 20}, tableWidth: 75, body: body, theme: "plain", //без границ styles: { font: "Proxima-normal", fontStyle:"italic" } }); }; async function generateTableAbout(doc, about, x, y, data) { let currentY = y; const iconX = 190; // Координата X для иконок (подобрана для правого края) const iconSize = 12; // Размер иконки в мм // Вспомогательная внутренняя функция, чтобы не дублировать код const addSection = async (title, body, iconPath, LinePath) => { // 1. Сначала загружаем картинки (ждем их) const iconImg = await loadImage(iconPath); const lineImg = LinePath ? await loadImage(LinePath) : null; // 2. РИСУЕМ ГРАФИКУ СРАЗУ (до таблицы!) // В этот момент currentY — это верхняя точка будущего раздела if (iconImg) { let iconYShift = 0; if (title === "О СЕБЕ") iconYShift = -3; else if (title === "ОПЫТ РАБОТЫ") iconYShift = -2.8; else if (title === "ОБРАЗОВАНИЕ") iconYShift = -2.8; else if (title === "ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ") iconYShift += 1; else if (title === "ДОСТИЖЕНИЯ") iconYShift = -2.8; // Рисуем иконку. Она попадет на текущую страницу. doc.addImage(iconImg, 'PNG', iconX, currentY + iconYShift, iconSize, iconSize); } if (lineImg) { let lineYShift = 0; let lineWidth = 30; let lineX = 155; if (title === "О СЕБЕ") { lineYShift = 2.75; lineWidth = 52; lineX = 133; } else if (title === "ОПЫТ РАБОТЫ") { lineYShift = 3; } else if (title === "ОБРАЗОВАНИЕ") { lineYShift = 2.5; } else if (title === "ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ"){ lineWidth = 15; lineYShift = 6.5; lineX = 170; } else if (title === "ДОСТИЖЕНИЯ"){ lineYShift = 3; lineWidth = 32; lineX = 155; } // Рисуем палочку. Она тоже попадет на текущую страницу. doc.addImage(lineImg, 'PNG', lineX, currentY + lineYShift, lineWidth, 0.3); } // 3. ТЕПЕРЬ РИСУЕМ ТАБЛИЦУ // Она начнется ровно в currentY, поверх или рядом с графикой doc.autoTable({ startY: currentY, // margin.top: 45 защитит от наезда на лого ТОЛЬКО на новых листах margin: { left: x, top: 25 }, theme: "plain", styles: { font: "Proxima-normal", cellPadding: 0, lineWidth: 0 }, body: body }); // 4. И только теперь обновляем Y для следующего раздела // Используем finalY, чтобы знать, где закончился текст currentY = doc.lastAutoTable.finalY + 10; }; // 1. ТАБЛИЦА: О СЕБЕ await addSection( "О СЕБЕ", [ [{ content: "О СЕБЕ", styles: { fontSize: 16, textColor: ['#6d5298'], fontStyle: "italic", cellPadding: { bottom: 2 } } }], [{ content: `Дата рождения: ${data.birthDate}`, styles: { fontSize: 12, textColor: ['#cf794b'] ,cellPadding: { left: 7 }} }], [{ content: about || "Не указано", styles: { fontSize: 14, cellPadding: { top: 2 } } }] ], 'images/о себе.png', 'images/палочка 1.png' ); // 2. ТАБЛИЦА: ОПЫТ РАБОТЫ let expBody = [[{ content: "ОПЫТ РАБОТЫ", styles: { fontSize: 16, textColor: ['#6d5298'], fontStyle: "italic", cellPadding: { bottom: 2 } } }]]; if (data.jobs.length === 0) { expBody.push([{ content: "Не указано", styles: { fontSize: 14 } }]); } else { data.jobs.forEach(job => { expBody.push([{ content: job.company, styles: { fontSize: 14, fontStyle: "bold", cellPadding: { top: 2 } } }]); expBody.push([{ content: job.role, styles: { fontSize: 14 } }]); expBody.push([{ content: `(${job.start} - ${job.end})`, styles: { fontSize: 12, textColor: [100, 100, 100] } }]); expBody.push([{ content: job.desc, styles: { fontSize: 13, cellPadding: { bottom: 3 } } }]); }); } await addSection("ОПЫТ РАБОТЫ", expBody, 'images/опыт.png', 'images/палочка 2.png'); // 3. ТАБЛИЦА: ОБРАЗОВАНИЕ let eduBody = [[{ content: "ОБРАЗОВАНИЕ", styles: { fontSize: 16, textColor: ['#6d5298'], fontStyle: "italic", cellPadding: { bottom: 2 } } }]]; if (data.education.length > 0) { data.education.forEach(edu => { eduBody.push([{ content: `${edu.lvl} - ${edu.school}`, styles: { fontSize: 14, cellPadding: { top: 2 } } }]); eduBody.push([{ content: `${edu.spec} | ${edu.year} г.`, styles: { fontSize: 12, cellPadding: { bottom: 2 } } }]); }); } else { eduBody.push([{ content: "Не указано", styles: { fontSize: 14 } }]); } await addSection("ОБРАЗОВАНИЕ", eduBody, 'images/образование.png', 'images/палочка 3.png'); //4.ДОСТИЖЕНИЯ let achBody = [[{content: "ДОСТИЖЕНИЯ", styles: { fontSize: 16, textColor: ['#6d5298'], fontStyle: "italic", cellPadding: { bottom: 2 } } }]]; await addSection( "ДОСТИЖЕНИЯ", [ [{ content: "ДОСТИЖЕНИЯ", styles: { fontSize: 16, textColor: ['#6d5298'], fontStyle: "italic", cellPadding: { bottom: 2 } } }], [{ content: data.achievements, styles: { fontSize: 14, cellPadding: { top: 1 } } }] ], 'images/достижения.png', "images/палочка 5.png" ); // 5. ТАБЛИЦА: ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ await addSection( "ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ", [ [{ content: "ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ", styles: { fontSize: 16, textColor: ['#6d5298'], fontStyle: "italic", cellPadding: { bottom: 2 } } }], [{ content: `Готовность к переезду: ${data.relocation}`, styles: { fontSize: 12, cellPadding: { top: 1 } } }], [{ content: `Водительские права: ${data.driverLicense}`, styles: { fontSize: 12 } }] ], 'images/допинфо.png', "images/палочка 4.png" ); } async function drawCircles(doc, data){ //let icon = await loadImage(`images/0 точка.png`); let x = 67; let y = 126.5; let icon = await loadImage(`images/${data.proskills["Деловая переписка"]} точка.png`); doc.addImage(icon, 'PNG', x, y, 28, 3.5); icon = await loadImage(`images/${data.proskills["Планирование занятости"]} точка.png`); doc.addImage(icon, 'PNG', x, y+=10, 28, 3.5); icon = await loadImage(`images/${data.proskills["Администрирование процессов"]} точка.png`); doc.addImage(icon, 'PNG', x, y+=12, 28, 3.5); icon = await loadImage(`images/${data.proskills["Корпоративная культура"]} точка.png`); doc.addImage(icon, 'PNG', x, y+=12, 28, 3.5); icon = await loadImage(`images/${data.proskills["Управление проектами"]} точка.png`); doc.addImage(icon, 'PNG', x, y+=11, 28, 3.5); //ОТРИСОВКА ПАЛОК НА ФИОЛЕТОВОМ ФОНЕ icon = await loadImage(`images/линия на фиолетовом фоне.png`); doc.addImage(icon, 'PNG', 25, 106.5, 62, 0.1); doc.addImage(icon, 'PNG', 25, 181.5, 62, 0.1); //ОТРИСОВКА ЗНАЧКОВ В ЛЕВОМ СТОЛБЦЕ icon = await loadImage(`images/телефон1.png`); doc.addImage(icon, 'PNG', 17, 78, 7, 7); icon = await loadImage(`images/почта1.png`); doc.addImage(icon, 'PNG', 17, 87.5, 7, 7); //icon = await loadImage(`images/телефон.png`); //doc.addImage(icon, 'PNG', 16, 77, 9, 9); //icon = await loadImage(`images/почта.png`); //doc.addImage(icon, 'PNG', 16, 86.5, 9, 9); icon = await loadImage(`images/вк.png`); doc.addImage(icon, 'PNG', 17, 96, 7, 7); icon = await loadImage(`images/локация.png`); doc.addImage(icon, 'PNG', 108, 55, 7, 7); icon = await loadImage(`images/люди.png`); doc.addImage(icon, 'PNG', 108, 84.5, 7, 7); } function drawTipZan(doc, text, x, y){ text = `Тип занятости: ${text}` doc.setFontSize(12); doc.setTextColor(0); doc.text(text, x, y) } function drawFormRab(doc, text, x, y){ text = `Форма работы: ${text}` doc.setFontSize(12); doc.setTextColor(0); doc.text(text, x, y) } function drawCity(doc, text, x, y){ text = `г. ${text}` doc.setFontSize(12); doc.setTextColor('#cf794b'); doc.text(text, x, y) } function getFormData() { const rawName = document.getElementById("fullName").value || "ФАМИЛИЯ ИМЯ ОТЧЕСТВО"; const parts = rawName.split(" "); // Сбор данных об опыте const jobs = []; document.querySelectorAll('.experience-item').forEach(item => { const isCurrent = item.querySelector('.exp-current').checked; const endDate = item.querySelector('.exp-end').value; jobs.push({ company: item.querySelector('.exp-company').value || "Компания", role: item.querySelector('.exp-role').value || "Должность", start: item.querySelector('.exp-start').value, end: isCurrent ? "по н.в." : (endDate || "—"), current: isCurrent, desc: item.querySelector('.exp-desc').value }); }); const education = []; document.querySelectorAll('.education-item').forEach(item => { education.push({ lvl: item.querySelector('.edu-lvl').options[item.querySelector('.edu-lvl').selectedIndex].text, school: item.querySelector('.edu-school').value || "Учебное заведение", spec: item.querySelector('.edu-spec').value || "Специальность", year: item.querySelector('.edu-year').value || "" }); }); const selectedLicenses = []; document.querySelectorAll('#driver-license-group input:checked').forEach(cb => { selectedLicenses.push(cb.value); }); return { name: [parts[0]?.toUpperCase() || "", `${parts[1] || ""} ${parts[2] || ""}`.trim()], phone: document.getElementById("phone").value || "Не указано", email: document.getElementById("email").value || "Не указано", vk: document.getElementById("vk").value || "Не указано", dolzh: document.getElementById("dolzh").value || "Не указано", zp: document.getElementById("zp").value || "Не указано", avatar: document.getElementById("avatar").files[0] || null, city: document.getElementById("city").value || "Не указано", birthDate: document.getElementById("birthDate").value || "Не указано", about: document.getElementById("about").value || "", jobs: jobs, formRab: document.querySelector('.formRab').options[document.querySelector('.formRab').selectedIndex].text, tipZan: document.querySelector('.tip-Zan').options[document.querySelector('.tip-Zan').selectedIndex].text, education: education, relocation: document.getElementById("relocation").value || "Не указано", driverLicense: selectedLicenses.length > 0 ? selectedLicenses.join(", ") : "Нет", proskills: getProfessionalSkills(), softskills: getSoftSkills(), achievements: document.getElementById("achievements").value || "Не указано" }; } function setupCropper() { const avatarInput = document.getElementById('avatar'); const imageToEdit = document.getElementById('image-to-edit'); const cropperContainer = document.getElementById('cropper-container'); const saveBtn = document.getElementById('save-crop'); avatarInput.addEventListener('change', function(e) { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(event) { imageToEdit.src = event.target.result; cropperContainer.style.display = 'block'; if (cropper) cropper.destroy(); // Инициализация Cropper.js cropper = new Cropper(imageToEdit, { aspectRatio: 1, // Квадрат viewMode: 1, autoCropArea: 1 }); }; reader.readAsDataURL(file); }); saveBtn.onclick = () => { const canvas = cropper.getCroppedCanvas({ width: 800, height: 800 }); // Сохраняем результат в глобальную переменную finalAvatarData = canvas.toDataURL('image/jpeg'); // Скрываем редактор cropperContainer.style.display = 'none'; }; } // --- Вспомогательные функции (остаются без изменений из вашего кода) --- async function loadFont(doc, fonts) { for (const font of fonts) { const response = await fetch(font.url); const buffer = await response.arrayBuffer(); const base64 = await arrayBufferToBase64(buffer); const fileName = font.url.split('/').pop(); doc.addFileToVFS(fileName, base64); doc.addFont(fileName, font.name, font.style); } doc.setFont("Proxima-normal", "normal"); } async function loadImage(url) { const response = await fetch(url); if (!response.ok) throw new Error(`Ошибка загрузки: ${response.statusText}`); const arrayBuffer = await response.arrayBuffer(); // Возвращаем как массив байтов return new Uint8Array(arrayBuffer); } function arrayBufferToBase64(buffer) { return new Promise(resolve => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result.split(',')[1]); reader.readAsDataURL(new Blob([buffer])); }); } function drawName(doc, nameLines, x, y) { doc.setFontSize(24); doc.setTextColor(0); doc.setFont("Proxima-normal", "italic"); doc.text(nameLines, x, y); doc.setFont("Proxima-normal", "normal"); } function drawDolzh(doc, text, x, y) { doc.setFontSize(18); doc.text(text, x, y); } function drawZp(doc, text, x, y) { doc.setFontSize(18); doc.text(`${text.replace(/\D/g, '')} руб.`, x, y); } function drawKontakt(doc, text, x, y) { doc.setFontSize(14); doc.setTextColor(255); doc.setFont("Proxima-normal", "italic"); doc.text(text, x, y); doc.setFont("Proxima-normal", "normal"); return y + 8; } function drawSectionTitle(doc, text, x, y) { doc.setFontSize(14); doc.setTextColor(0); doc.text(text, x, y); return y + 8; } function drawTextLine(doc, text, x, y) { doc.setFontSize(10); doc.text(text, x, y); return y + 6; } function drawParagraph(doc, text, x, y, width) { doc.setFontSize(10); const lines = doc.splitTextToSize(text, width); doc.text(lines, x, y); return y + (lines.length * 5) + 5; } async function drawAva(doc, file, x, y) { // Теперь мы игнорируем аргумент file и берем обрезанное фото if (finalAvatarData) { doc.addImage(finalAvatarData, "JPEG", x, y, 40, 40); } } async function getCroppedCircleImage(file, size = 200) { return new Promise((resolve) => { const img = new Image(); const reader = new FileReader(); reader.onload = (e) => img.src = e.target.result; img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = size; canvas.height = size; const ctx = canvas.getContext("2d"); ctx.beginPath(); ctx.arc(size/2, size/2, size/2, 0, Math.PI*2); ctx.clip(); const minSide = Math.min(img.width, img.height); ctx.drawImage(img, (img.width-minSide)/2, (img.height-minSide)/2, minSide, minSide, 0, 0, size, size); resolve(canvas.toDataURL("image/png")); }; reader.readAsDataURL(file); }); } function sendGS(dataa){ const url = 'https://script.google.com/macros/s/AKfycbyXpxzQVBOt1nd-Y2lb2AOzqPnUxC55GkeUmz9NYLpnhsWOqivUCbryyQj1Q15KF7Ma/exec'; const data = { token: "MySeCrEttttttOkEn", name: dataa.name, birthday: dataa.birthday, phone: dataa.phone, email : dataa.email, tg: dataa.vk, dolzh: dataa.dolzh, zp: dataa.zp, tipZan: dataa.tipZan, formRab: dataa.formRab, city: dataa.city, about: dataa.about, relocation: dataa.relocation, driverLicense: dataa.driverLicense, jobs: dataa.jobs, education: dataa.education, proskills: dataa.proskills, softskills: dataa.softskills }; fetch(url, { method: 'POST', mode: 'no-cors', // Важно для Apps Script body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }) }