!DOCTYPE html html lang=ru head meta charset=UTF-8 titleФорма для менеджеров по продажамtitle style Обновленные стили с новым дизайном @import url('httpsfonts.googleapis.comcss2family=Interwght@400;500;600;700&display=swap'); root { --primary #2563eb; --primary-dark #1d4ed8; --primary-light #60a5fa; --success #10a37f; --success-light #34d399; --danger #ef4444; --danger-light #f87171; --warning #f59e0b; --purple #8b5cf6; --purple-light #a78bfa; --background #ffffff; --surface #f8fafc; --text-primary #1e293b; --text-secondary #475569; --border #cbd5e1; --shadow-sm 0 1px 3px 0 rgb(0 0 0 0.1); --shadow 0 4px 6px -1px rgb(0 0 0 0.1); --shadow-md 0 6px 12px -2px rgb(0 0 0 0.1); --radius 8px; --radius-lg 12px; --gradient-primary linear-gradient(135deg, #2563eb 0%, #3b82f6 100%); --gradient-success linear-gradient(135deg, #10a37f 0%, #34d399 100%); --gradient-danger linear-gradient(135deg, #ef4444 0%, #f87171 100%); --gradient-purple linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%); --gradient-warning linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%); } { box-sizing border-box; } body { background #f1f5f9; font-family 'Inter', system-ui, -apple-system, sans-serif; margin 0; padding 0; min-height 100vh; color var(--text-primary); line-height 1.5; font-weight 400; } .container { max-width 1200px; margin 30px auto; padding 0 20px; display grid; grid-template-columns 320px 1fr; gap 24px; } .org-info-box { background var(--background); border-radius var(--radius-lg); box-shadow var(--shadow-md); padding 24px; border 1px solid var(--border); position relative; overflow hidden; } .form-box { background var(--background); border-radius var(--radius-lg); box-shadow var(--shadow-md); padding 32px; border 1px solid var(--border); } h2 { font-size 1.5rem; font-weight 600; margin-bottom 8px; color var(--text-primary); } h3 { font-size 1.25rem; font-weight 600; margin-bottom 20px; color var(--text-primary); } .progress-container { background linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); border-radius var(--radius); padding 20px; margin-bottom 24px; text-align center; border 1px solid var(--border); position relative; overflow hidden; } .progress-containerbefore { content ''; position absolute; top 0; left 0; right 0; height 4px; background linear-gradient(90deg, var(--primary) 0%, var(--success) 100%); } .progress-percent { font-size 2rem; font-weight 700; color transparent; margin-bottom 4px; background linear-gradient(135deg, var(--primary) 0%, var(--success) 100%); -webkit-background-clip text; background-clip text; position relative; display inline-block; text-shadow 0 2px 4px rgba(0, 0, 0, 0.1); } .progress-label { font-size 0.875rem; color var(--text-secondary); font-weight 600; text-transform uppercase; letter-spacing 0.05em; } .form-group { margin-bottom 20px; } label { font-size 0.875rem; font-weight 500; margin-bottom 6px; display block; color var(--text-primary); } .required { color var(--danger); font-weight 600; } input, select, textarea { width 100%; padding 12px 16px; border 2px solid var(--border); border-radius var(--radius); font-size 0.875rem; background var(--surface); color var(--text-primary); transition all 0.2s ease; outline none; font-family 'Inter', sans-serif; font-weight 400; } inputhover, selecthover, textareahover { border-color var(--primary-light); } inputfocus, selectfocus, textareafocus { border-color var(--primary); box-shadow 0 0 0 3px rgba(37, 99, 235, 0.1); } input[type=number] { font-variant-numeric tabular-nums; } textarea { min-height 100px; resize vertical; font-size 0.875rem; line-height 1.5; } .field-hint { font-size 0.75rem; color var(--text-secondary); margin 4px 0 8px 0; } Стиль для квадратных радиокнопок .inline-checkbox { display flex; flex-wrap wrap; gap 12px; margin-top 8px; } .checkbox-option { position relative; } .checkbox-option input[type=radio] { position absolute; opacity 0; width 0; height 0; } .checkbox-label { display flex; align-items center; justify-content center; gap 8px; padding 10px 16px; background var(--surface); border 2px solid var(--border); border-radius var(--radius); font-size 0.875rem; font-weight 500; color var(--text-primary); cursor pointer; transition all 0.2s ease; box-shadow var(--shadow-sm); min-width 120px; text-align center; } .checkbox-labelhover { border-color var(--primary-light); transform translateY(-1px); box-shadow var(--shadow); } .checkbox-option input[type=radio]checked + .checkbox-label { background linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%); border-color var(--primary); color white; box-shadow var(--shadow-md); } .checkbox-option input[type=radio]checked + .checkbox-labelbefore { content '✓'; font-weight 600; } .section-title { font-size 1rem; font-weight 600; color var(--text-primary); margin 24px 0 12px 0; padding-bottom 8px; border-bottom 2px solid var(--border); } button { padding 12px 24px; background var(--gradient-primary); color white; border none; border-radius var(--radius); font-size 0.875rem; font-weight 600; cursor pointer; transition all 0.3s ease; box-shadow var(--shadow); position relative; overflow hidden; } buttonbefore { content ''; position absolute; top -50%; left -100%; width 200%; height 200%; background linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.3), transparent); transform rotate(45deg); transition left 0.7s ease; } buttonhoverbefore { left 100%; } buttonhover { transform translateY(-2px); box-shadow var(--shadow-md); } buttonactive { transform translateY(0); } buttondisabled { background #94a3b8; cursor not-allowed; transform none; box-shadow none; } buttondisabledbefore { display none; } Кнопка Отправить теперь зеленая .btn-green-submit { background var(--gradient-success) !important; } .btn-green-submithover { background linear-gradient(135deg, #059669 0%, #34d399 100%) !important; } .btn-purple { background var(--gradient-purple) !important; display flex; align-items center; justify-content center; gap 8px; width 100%; margin-top 8px; } .btn-purplehover { background linear-gradient(135deg, #6d28d9 0%, #a78bfa 100%) !important; } .btn-green { background var(--gradient-success) !important; margin-top 16px; display flex; align-items center; justify-content center; gap 8px; font-size 0.875rem; width 100%; } .btn-greenhover { background linear-gradient(135deg, #059669 0%, #34d399 100%) !important; } .btn-outline { background transparent !important; border 2px solid var(--border); color var(--text-primary); } .btn-outlinehover { background var(--surface) !important; border-color var(--primary); color var(--primary); } .error-field { border-color var(--danger) !important; background #fef2f2; } .error-message { color var(--danger); font-size 0.75rem; margin-top 4px; font-weight 500; display none; } .error-message.active { display block; } .hidden { display none !important; } .step { display none; } .step.active { display block; animation fadeIn 0.3s ease; } @keyframes fadeIn { from { opacity 0; transform translateY(10px); } to { opacity 1; transform translateY(0); } } .loading-indicator { display inline-block; width 16px; height 16px; border 2px solid rgba(255, 255, 255, 0.3); border-top 2px solid white; border-radius 50%; animation spin 1s linear infinite; } @keyframes spin { 0% { transform rotate(0deg); } 100% { transform rotate(360deg); } } .notification { position fixed; top 20px; right 20px; padding 12px 20px; border-radius var(--radius); color white; font-weight 500; z-index 10000; box-shadow var(--shadow-md); animation slideIn 0.3s ease; max-width 350px; font-size 0.875rem; } .notification.success { background var(--gradient-success); } .notification.error { background var(--gradient-danger); } .notification.warning { background var(--gradient-warning); } .notification.info { background var(--gradient-primary); } @keyframes slideIn { from { transform translateX(100%); opacity 0; } to { transform translateX(0); opacity 1; } } .visually-hidden { position absolute; width 1px; height 1px; margin -1px; padding 0; overflow hidden; clip rect(0, 0, 0, 0); border 0; } Стили для чекбокса Торги не проведены .trading-checkbox { display flex; align-items center; gap 8px; margin-top 8px; padding 8px 12px; background var(--surface); border-radius var(--radius); border 1px solid var(--border); } .trading-checkbox input[type=checkbox] { width 16px; height 16px; cursor pointer; } .trading-checkbox label { margin 0; font-size 0.875rem; cursor pointer; color var(--text-primary); } Стили для дней между датами .days-counter { font-size 0.875rem; font-weight 600; color var(--primary); background linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); padding 6px 12px; border-radius var(--radius); margin-left 12px; white-space nowrap; border 1px solid #bfdbfe; } Инструкция компактная .compact-instruction { font-size 0.75rem; color var(--text-secondary); margin-top 16px; padding 12px; background linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); border-radius var(--radius); border 1px solid var(--border); } .compact-instruction ol { margin 8px 0 0 0; padding-left 20px; } .compact-instruction li { margin-bottom 4px; } Кнопки навигации .button-group { display flex; gap 12px; margin-top 24px; } .button-group button { flex 1; } Стили для второй страницы .form-footer { margin-top 32px; padding-top 24px; border-top 2px solid var(--border); } Адаптивность @media (max-width 1024px) { .container { grid-template-columns 1fr; gap 20px; } .org-info-box, .form-box { width 100%; } } @media (max-width 768px) { .container { padding 16px; margin 16px auto; } .form-box { padding 24px; } h2 { font-size 1.25rem; } h3 { font-size 1.125rem; } .progress-percent { font-size 1.75rem; } .inline-checkbox { gap 8px; } .checkbox-label { min-width 100px; padding 8px 12px; } .button-group { flex-direction column; } } style script src=httpscdnjs.cloudflare.comajaxlibsjspdf2.5.1jspdf.umd.min.jsscript script src=httpscdnjs.cloudflare.comajaxlibshtml2canvas1.4.1html2canvas.min.jsscript head body div class=container !-- Бокс с информацией об организации -- div class=org-info-box h2Организацияh2 div class=progress-container div class=progress-labelЗАПОЛНЕНОdiv div class=progress-percent id=progress-percent aria-live=polite0%div div div class=form-group label for=org_innИНН организации span class=requiredspanlabel div class=field-hintВведите 10 цифр для ООО или 12 цифр для ИПdiv input type=text name=org_inn id=org_inn required maxlength=12 placeholder=Введите ИНН div id=inn_error class=error-message role=alertИНН должен содержать span id=inn_length10 или 12span цифрdiv div div class=form-group label for=org_nameНаименование организации span class=requiredspanlabel div class=field-hintАвтоматически заполняется при загрузке данныхdiv input type=text name=org_name id=org_name required readonly div button type=button id=dadata-btn class=btn-green span id=dadata-iconspan span id=dadata-textЗагрузить данныеspan button div class=compact-instruction ol liВведите ИНН организацииli liНажмите Загрузить данныеli liЗаполните остальные поляli ol div div !-- Основная форма -- div class=form-box form id=form-step1 class=step active autocomplete=off h3Основная информацияh3 div class=form-group label for=fz_typeВид ФЗ span class=requiredspanlabel select name=fz_type id=fz_type required option value=Выберите ФЗoption option value=44-ФЗ44-ФЗoption option value=223-ФЗ223-ФЗoption option value=185-ФЗ, 615 ПП185-ФЗ, 615 ППoption option value=275-ФЗ275-ФЗoption option value=ФТСФТСoption option value=ФНСФНСoption option value=Коммерческая закупкаКоммерческая закупкаoption select div div class=form-group label for=bg_typeВид БГ span class=requiredspanlabel select name=bg_type id=bg_type required option value=Выберите видoption option value=ИсполнениеИсполнениеoption option value=Исполнение с авансомИсполнение с авансомoption option value=УчастиеУчастиеoption option value=Возврат авансового платежаВозврат авансового платежаoption option value=Гарантийные обязательстваГарантийные обязательстваoption select div div id=commercial_block class=hidden div class=section-titleКоммерческая закупкаdiv div class=form-group label for=commercial_platform_nameНаименование площадки span class=requiredspanlabel input type=text name=commercial_platform_name id=commercial_platform_name div div class=form-group label for=commercial_contract_numberНомер контрактадоговора span class=requiredspanlabel input type=text name=commercial_contract_number id=commercial_contract_number div div class=form-group label for=commercial_customer_nameНаименование заказчика (Бенефициар) span class=requiredspanlabel input type=text name=commercial_customer_name id=commercial_customer_name div div class=form-group label for=commercial_customer_innИНН заказчика span class=requiredspanlabel input type=text name=commercial_customer_inn id=commercial_customer_inn maxlength=10 placeholder=10 цифр div id=commercial_inn_error class=error-messageИНН должен содержать ровно 10 цифрdiv div div class=form-group label for=commercial_contract_subjectПредмет контрактадоговора span class=requiredspanlabel input type=text name=commercial_contract_subject id=commercial_contract_subject div div class=form-group label for=commercial_contract_urlСсылка на контрактдоговорlabel input type=url name=commercial_contract_url id=commercial_contract_url placeholder=https... div div div class=form-group id=eis_url_block label for=eis_urlСсылка на ЕИСаукционlabel input type=url name=eis_url id=eis_url placeholder=https... div div class=form-group id=rnt_block label for=rntРНТ (реестровый номер торгов)label input type=text name=rnt id=rnt maxlength=32 placeholder=Только цифры div id=eis_or_rnt_error class=error-messageЗаполните хотя бы одно из полей Ссылка на ЕИСаукцион или РНТdiv div div class=section-titleЗакрытая закупка span class=requiredspandiv div class=inline-checkbox div class=checkbox-option input type=radio name=closed_purchase value=да id=closed_yes required label for=closed_yes class=checkbox-labelДаlabel div div class=checkbox-option input type=radio name=closed_purchase value=нет id=closed_no label for=closed_no class=checkbox-labelНетlabel div div div id=closed_purchase_block class=hidden div class=form-group label for=platform_nameНаименование площадки span class=requiredspanlabel input type=text name=platform_name id=platform_name div div class=form-group label for=contract_numberНомер контрактадоговора span class=requiredspanlabel input type=text name=contract_number id=contract_number div div class=form-group label for=customer_nameНаименование заказчика (Бенефициар) span class=requiredspanlabel input type=text name=customer_name id=customer_name div div class=form-group label for=customer_innИНН заказчика span class=requiredspanlabel input type=text name=customer_inn id=customer_inn maxlength=10 placeholder=10 цифр div id=closed_inn_error class=error-messageИНН должен содержать ровно 10 цифрdiv div div class=form-group label for=contract_subjectПредмет контрактадоговора span class=requiredspanlabel input type=text name=contract_subject id=contract_subject div div class=form-group label for=contract_urlСсылка на контрактдоговорlabel input type=url name=contract_url id=contract_url placeholder=https... div div div class=section-titleЗакупка у единственного поставщика span class=requiredspandiv div class=inline-checkbox div class=checkbox-option input type=radio name=single_supplier value=да id=single_yes required label for=single_yes class=checkbox-labelДаlabel div div class=checkbox-option input type=radio name=single_supplier value=нет id=single_no label for=single_no class=checkbox-labelНетlabel div div div class=section-titleПереобеспечение span class=requiredspandiv div class=inline-checkbox div class=checkbox-option input type=radio name=reinsurance value=нет id=reinsurance_no required label for=reinsurance_no class=checkbox-labelНетlabel div div class=checkbox-option input type=radio name=reinsurance value=денежные средства id=reinsurance_cash label for=reinsurance_cash class=checkbox-labelДенежные средстваlabel div div class=checkbox-option input type=radio name=reinsurance value=другой банк id=reinsurance_bank label for=reinsurance_bank class=checkbox-labelДругой банкlabel div div class=checkbox-option input type=radio name=reinsurance value=изменения id=reinsurance_changes label for=reinsurance_changes class=checkbox-labelИзмененияlabel div div div id=bank_name_block class=form-group hidden label for=bank_name_inputНаименование банка, выпустившего БГ span class=requiredspanlabel input type=text name=bank_name id=bank_name_input div id=bank_name_error class=error-messageЗаполните это полеdiv div div class=button-group button type=button id=to_step2Далее →button div form form id=form-step2 class=step autocomplete=off h3Детали сделкиh3 div class=form-group label for=nmckНМЦК контракта span class=requiredspanlabel div class=field-hintНачальная (максимальная) цена контрактаdiv input type=text name=nmck id=nmck required placeholder=Введите сумму class=amount-input div div class=form-group label for=offer_priceПредложенная цена span class=requiredspanlabel input type=text name=offer_price id=offer_price required placeholder=0,00 class=amount-input div class=trading-checkbox input type=checkbox id=no_trading_checkbox label for=no_trading_checkboxТорги не проведеныlabel div div div class=form-group label for=bg_amountСумма БГ span class=requiredspanlabel div class=field-hintСумма банковской гарантииdiv input type=text name=bg_amount id=bg_amount required placeholder=Введите сумму class=amount-input div div class=section-titleЕсть ли аванс span class=requiredspandiv div class=inline-checkbox div class=checkbox-option input type=radio name=has_advance value=да id=advance_yes required label for=advance_yes class=checkbox-labelДаlabel div div class=checkbox-option input type=radio name=has_advance value=нет id=advance_no label for=advance_no class=checkbox-labelНетlabel div div div id=advance_block class=hidden div class=form-group label for=advance_amountСумма аванса span class=requiredspanlabel div class=field-hint id=advance_percent_hintdiv input type=text name=advance_amount id=advance_amount placeholder=0,00 class=amount-input div div div class=form-group label for=bg_start_dateНачало действия БГ span class=requiredspanlabel input type=date name=bg_start_date id=bg_start_date required div div class=form-group label for=bg_dateОкончание действия БГ span class=requiredspanlabel div style=display flex; align-items center; gap 12px; input type=date name=bg_date id=bg_date required style=flex 1; span id=days_diff class=days-counter aria-label=дней между датамиspan div div div class=form-group label for=commentКомментарийlabel textarea name=comment id=comment rows=4 placeholder=Дополнительная информация (не обязательно) style=white-space pre-wrap; word-wrap break-word;textarea div div class=form-footer div class=button-group button type=button id=to_step1 class=btn-outline← Назадbutton button type=submit id=submit_form class=btn-green-submitОтправитьbutton div button type=button id=export_csv class=btn-purple Экспорт в Excel button div form div div div id=pdf-preview style=positionfixed;top-20000px;left-20000px;width800px;background#ffffff;padding0;div script --- Базовые переменные --- const step1 = document.getElementById('form-step1'); const step2 = document.getElementById('form-step2'); const toStep2 = document.getElementById('to_step2'); const toStep1 = document.getElementById('to_step1'); const dadataBtn = document.getElementById('dadata-btn'); const dadataIcon = document.getElementById('dadata-icon'); const dadataText = document.getElementById('dadata-text'); const orgInn = document.getElementById('org_inn'); const orgName = document.getElementById('org_name'); --- Ваш API-ключ DaData --- const DADATA_API_KEY = 'd577fd510245285d56bdaf534c1aaa557ca92fe3'; --- Функция для форматирования суммы с пробелами --- function formatAmount(value) { if (!value) return ''; Удаляем все пробелы и запятые let cleanValue = value.toString().replace(s+g, '').replace(',', '.'); Если значение пустое или только точкизапятые if (cleanValue === '' cleanValue === '.' cleanValue === ',') return ''; Разделяем на целую и дробную части let [integerPart, decimalPart] = cleanValue.split('.'); Если нет дробной части, проверяем запятую if (!decimalPart && cleanValue.includes(',')) { [integerPart, decimalPart] = cleanValue.split(','); } Удаляем все нецифровые символы из целой части integerPart = integerPart.replace(Dg, ''); Форматируем целую часть с пробелами let formattedInteger = ''; for (let i = integerPart.length - 1, count = 0; i = 0; i--, count++) { formattedInteger = integerPart[i] + formattedInteger; if (count % 3 === 2 && i 0) { formattedInteger = ' ' + formattedInteger; } } Обрабатываем дробную часть let result = formattedInteger; if (decimalPart !== undefined) { Удаляем все нецифровые символы из дробной части decimalPart = decimalPart.replace(Dg, ''); Ограничиваем до 2 знаков decimalPart = decimalPart.substring(0, 2); if (decimalPart.length 0) { result += ',' + decimalPart; } } return result; } --- Функция для преобразования форматированной суммы обратно в число --- function parseFormattedAmount(formattedValue) { if (!formattedValue) return ''; Удаляем пробелы, заменяем запятую на точку return formattedValue.replace(s+g, '').replace(',', '.'); } --- Обработчики для полей с суммами --- function setupAmountInputs() { const amountInputs = document.querySelectorAll('.amount-input'); amountInputs.forEach(input = { Форматируем при вводе input.addEventListener('input', function() { const cursorPosition = this.selectionStart; const oldValue = this.value; const formattedValue = formatAmount(oldValue); this.value = formattedValue; Восстанавливаем позицию курсора if (formattedValue.length oldValue.length) { this.setSelectionRange(cursorPosition + 1, cursorPosition + 1); } else if (formattedValue.length oldValue.length) { this.setSelectionRange(cursorPosition - 1, cursorPosition - 1); } else { this.setSelectionRange(cursorPosition, cursorPosition); } updateProgress(); }); При фокусе убираем форматирование для удобства редактирования input.addEventListener('focus', function() { this.dataset.originalValue = this.value; this.value = parseFormattedAmount(this.value); }); При потере фокуса снова форматируем input.addEventListener('blur', function() { if (this.value !== this.dataset.originalValue) { this.value = formatAmount(this.value); } }); Обработка вставки input.addEventListener('paste', function(e) { e.preventDefault(); const text = (e.clipboardData window.clipboardData).getData('text'); const formatted = formatAmount(text); Вставляем форматированное значение document.execCommand('insertText', false, formatted); }); }); } --- Функция для показа уведомлений --- function showNotification(message, type = 'info') { const oldNotifications = document.querySelectorAll('.notification'); oldNotifications.forEach(notif = notif.remove()); const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.textContent = message; notification.setAttribute('role', 'alert'); notification.setAttribute('aria-live', 'polite'); document.body.appendChild(notification); setTimeout(() = { notification.style.animation = 'slideOut 0.3s ease'; setTimeout(() = notification.remove(), 300); }, 4000); } --- Функция автозаполнения с DaData API --- async function fetchCompanyDataByINN(inn) { if ((inn.length === 10 inn.length === 12) && ^d+$.test(inn)) { try { dadataBtn.disabled = true; dadataIcon.innerHTML = 'span class=loading-indicatorspan'; dadataText.textContent = 'Загрузка...'; const response = await fetch('httpssuggestions.dadata.rusuggestionsapi4_1rsfindByIdparty', { method 'POST', mode 'cors', headers { 'Content-Type' 'applicationjson', 'Accept' 'applicationjson', 'Authorization' `Token ${DADATA_API_KEY}` }, body JSON.stringify({ query inn, count 1, branch_type 'MAIN' }) }); if (!response.ok) { throw new Error(`HTTP ошибка ${response.status}`); } const data = await response.json(); if (data.suggestions && data.suggestions.length 0) { const company = data.suggestions[0]; const companyData = company.data; orgName.value = company.value; let orgType = ''; if (companyData.type === 'INDIVIDUAL') { orgType = 'ИП'; } else if (companyData.type === 'LEGAL') { if (companyData.opf && companyData.opf.short) { orgType = companyData.opf.short; } else { orgType = 'Юридическое лицо'; } } let additionalInfo = ''; if (orgType) { additionalInfo += `Тип ${orgType}`; } if (companyData.ogrn) { if (additionalInfo) additionalInfo += ' • '; additionalInfo += `ОГРН ${companyData.ogrn}`; } if (companyData.address && companyData.address.value) { if (additionalInfo) additionalInfo += ' • '; additionalInfo += `Адрес ${companyData.address.value}`; } if (companyData.management && companyData.management.name) { if (additionalInfo) additionalInfo += ' • '; additionalInfo += `Руководитель ${companyData.management.name}`; } if (additionalInfo) { showNotification(`Загружено ${company.value}. ${additionalInfo}`, 'success'); } else { showNotification(`Загружено ${company.value}`, 'success'); } } else { showNotification('Организация по указанному ИНН не найдена', 'warning'); orgName.value = ''; } } catch (error) { console.error('Ошибка получения данных от DaData', error); if (error.message.includes('401') error.message.includes('403')) { showNotification('Ошибка авторизации API. Проверьте API ключ.', 'error'); } else if (error.message.includes('429')) { showNotification('Превышен лимит запросов к DaData', 'error'); } else { showNotification('Ошибка подключения к сервису DaData', 'error'); } orgName.value = ''; } finally { dadataBtn.disabled = false; dadataIcon.textContent = ''; dadataText.textContent = 'Загрузить данные'; updateProgress(); } } } --- ИСПРАВЛЕННАЯ ПРОСТАЯ Индикация прогресса --- function updateProgress() { let filledCount = 0; let totalRequired = 0; Получаем активный шаг const activeStep = step1.classList.contains('active') step1 step2; const isStep1 = activeStep === step1; Функция проверки заполненности поля function isFieldFilled(field) { if (!field field.closest('.hidden')) return false; if (field.type === 'radio') { const name = field.name; const checked = document.querySelector(`[name=${name}]checked`); return checked && !checked.closest('.hidden'); } else if (field.type === 'checkbox') { return field.checked; } else if (field.type === 'select-one') { return field.value && field.value !== ''; } else { return field.value && field.value.toString().trim() !== ''; } } Функция проверки видимости элемента function isElementVisible(element) { if (!element) return false; if (element.closest('.hidden')) return false; if (element.offsetParent === null) return false; if (getComputedStyle(element).display === 'none') return false; if (getComputedStyle(element).visibility === 'hidden') return false; return true; } Список всех обязательных полей на форме const allRequiredFields = [ Шаг 1 { id 'org_inn', step 'both' }, { id 'org_name', step 'both' }, { id 'fz_type', step 1 }, { id 'bg_type', step 1 }, Радиогруппы { name 'closed_purchase', step 1, type 'radio' }, { name 'single_supplier', step 1, type 'radio' }, { name 'reinsurance', step 1, type 'radio' }, Динамические поля шага 1 { id 'commercial_platform_name', step 1, dependsOn 'commercial' }, { id 'commercial_contract_number', step 1, dependsOn 'commercial' }, { id 'commercial_customer_name', step 1, dependsOn 'commercial' }, { id 'commercial_customer_inn', step 1, dependsOn 'commercial' }, { id 'commercial_contract_subject', step 1, dependsOn 'commercial' }, { id 'platform_name', step 1, dependsOn 'closed' }, { id 'contract_number', step 1, dependsOn 'closed' }, { id 'customer_name', step 1, dependsOn 'closed' }, { id 'customer_inn', step 1, dependsOn 'closed' }, { id 'contract_subject', step 1, dependsOn 'closed' }, { id 'bank_name_input', step 1, dependsOn 'bank' }, Шаг 2 { id 'nmck', step 2 }, { id 'offer_price', step 2, condition () = !document.getElementById('no_trading_checkbox').checked }, { id 'bg_amount', step 2 }, { name 'has_advance', step 2, type 'radio' }, { id 'advance_amount', step 2, condition () = document.querySelector('input[name=has_advance]checked').value === 'да' }, { id 'bg_start_date', step 2 }, { id 'bg_date', step 2 } ]; Проверяем каждое поле allRequiredFields.forEach(fieldDef = { Проверяем, нужно ли учитывать это поле на текущем шаге const shouldCheck = fieldDef.step === 'both' (isStep1 && fieldDef.step === 1) (!isStep1 && fieldDef.step === 2); if (!shouldCheck) return; Проверяем условия зависимости let shouldInclude = true; if (fieldDef.dependsOn === 'commercial') { shouldInclude = document.getElementById('fz_type').value === 'Коммерческая закупка'; } else if (fieldDef.dependsOn === 'closed') { shouldInclude = document.querySelector('input[name=closed_purchase]checked').value === 'да'; } else if (fieldDef.dependsOn === 'bank') { shouldInclude = document.querySelector('input[name=reinsurance]checked').value === 'другой банк'; } if (!shouldInclude) return; Проверяем дополнительные условия if (fieldDef.condition && !fieldDef.condition()) return; let field; if (fieldDef.type === 'radio') { Для радиогрупп проверяем наличие выбранного варианта const radios = document.querySelectorAll(`[name=${fieldDef.name}]`); const checkedRadio = Array.from(radios).find(radio = radio.checked && isElementVisible(radio)); field = checkedRadio radios[0]; Используем первый радио для проверки } else { field = document.getElementById(fieldDef.id); } if (!field) return; Проверяем видимость поля if (!isElementVisible(field)) return; totalRequired++; if (isFieldFilled(field)) { filledCount++; } }); Для шага 2 добавляем проверку EISRNT если это не коммерческая закупка if (!isStep1) { const fzTypeValue = document.getElementById('fz_type').value; if (fzTypeValue !== 'Коммерческая закупка') { const eisUrl = document.getElementById('eis_url').value; const rnt = document.getElementById('rnt').value; totalRequired++; if (eisUrl.trim() !== '' rnt.trim() !== '') { filledCount++; } } } Рассчитываем процент const percentage = totalRequired 0 Math.min(100, Math.round((filledCount totalRequired) 100)) 0; const percentElement = document.getElementById('progress-percent'); percentElement.textContent = percentage + '%'; Обновляем градиент в зависимости от процента updateProgressColor(percentage, percentElement); } function updateProgressColor(percentage, element) { if (percentage 30) { element.style.background = 'linear-gradient(135deg, #ef4444 0%, #f87171 100%)'; } else if (percentage 70) { element.style.background = 'linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)'; } else if (percentage 100) { element.style.background = 'linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)'; } else { element.style.background = 'linear-gradient(135deg, #10a37f 0%, #34d399 100%)'; } element.style.webkitBackgroundClip = 'text'; element.style.backgroundClip = 'text'; } function showStep(stepNum) { if(stepNum === 1) { step1.classList.add('active'); step2.classList.remove('active'); } else { step2.classList.add('active'); step1.classList.remove('active'); } updateProgress(); } --- Валидация ИНН организации --- const innError = document.getElementById('inn_error'); const innLengthLabel = document.getElementById('inn_length'); function getInnLength() { const innValue = orgInn.value.replace(Dg, ''); if (innValue.length === 10) return 10; if (innValue.length === 12) return 12; return 0; } function updateDadataButton() { const innValue = orgInn.value.replace(Dg, ''); const requiredLength = getInnLength(); if (innValue.length === requiredLength && requiredLength 0) { dadataBtn.disabled = false; dadataBtn.title = 'Нажмите для загрузки данных организации'; } else { dadataBtn.disabled = true; dadataBtn.title = 'Введите корректный ИНН для загрузки данных'; } } function validateInn() { const innValue = orgInn.value.replace(Dg, ''); const len = innValue.length === 10 10 innValue.length === 12 12 0; if (len === 0 innValue.length !== len) { innLengthLabel.textContent = '10 или 12'; innError.style.display = 'block'; innError.setAttribute('role', 'alert'); return false; } innError.style.display = 'none'; updateDadataButton(); return true; } orgInn.addEventListener('input', function() { if (orgName.value) { orgName.value = ''; } validateInn(); updateProgress(); }); dadataBtn.addEventListener('click', function() { if (!dadataBtn.disabled && orgInn.value.trim() !== '') { fetchCompanyDataByINN(orgInn.value.replace(Dg, '')); } }); orgInn.addEventListener('blur', function() { if (validateInn() && this.value.trim() !== '') { setTimeout(() = { if (orgName.value.trim() === '') { fetchCompanyDataByINN(this.value.replace(Dg, '')); } }, 1000); } }); --- Остальной код формы --- function onlyDigitsInput(input, maxLength=0) { input.addEventListener('input', function() { let numbers = this.value.replace(Dg, ''); if (maxLength 0) numbers = numbers.substring(0, maxLength); this.value = numbers; }); input.addEventListener('paste', function(e) { e.preventDefault(); let text = (e.clipboardData window.clipboardData).getData('text'); let numbers = text.replace(Dg, ''); if (maxLength 0) numbers = numbers.substring(0, maxLength); document.execCommand('insertText', false, numbers); }); } onlyDigitsInput(document.getElementById('org_inn'), 12); onlyDigitsInput(document.getElementById('rnt'), 32); onlyDigitsInput(document.getElementById('customer_inn'), 10); onlyDigitsInput(document.getElementById('commercial_customer_inn'), 10); const fzType = document.getElementById('fz_type'); const commercialBlock = document.getElementById('commercial_block'); const eisUrlBlock = document.getElementById('eis_url_block'); const rntBlock = document.getElementById('rnt_block'); fzType.addEventListener('change', function() { if (fzType.value === 'Коммерческая закупка') { commercialBlock.classList.remove('hidden'); eisUrlBlock.classList.add('hidden'); rntBlock.classList.add('hidden'); document.getElementById('commercial_customer_inn').classList.remove('error-field'); document.getElementById('commercial_inn_error').classList.remove('active'); } else { commercialBlock.classList.add('hidden'); eisUrlBlock.classList.remove('hidden'); rntBlock.classList.remove('hidden'); } updateProgress(); }); function validateCommercialFields() { let valid = true; const ids = [ 'commercial_platform_name', 'commercial_contract_number', 'commercial_customer_name', 'commercial_customer_inn', 'commercial_contract_subject' ]; ids.forEach(id = { const el = document.getElementById(id); if (!el.value.trim() (id === 'commercial_customer_inn' && el.value.length !== 10)) { el.classList.add('error-field'); valid = false; } else { el.classList.remove('error-field'); } }); validateCommercialInn(); return valid && validateCommercialInn(); } function validateCommercialInn() { const input = document.getElementById('commercial_customer_inn'); const error = document.getElementById('commercial_inn_error'); let val = input.value.replace(Dg, ''); input.value = val; if (fzType.value === 'Коммерческая закупка' && commercialBlock.classList.contains('hidden') === false) { if (val.length !== 10) { input.classList.add('error-field'); error.classList.add('active'); error.setAttribute('role', 'alert'); return false; } } input.classList.remove('error-field'); error.classList.remove('active'); return true; } document.getElementById('commercial_customer_inn').addEventListener('input', function() { validateCommercialInn(); updateProgress(); }); function validateEISorRNT() { const eis = document.getElementById('eis_url'); const rnt = document.getElementById('rnt'); const error = document.getElementById('eis_or_rnt_error'); if (fzType.value !== 'Коммерческая закупка') { if (!eis.value.trim() && !rnt.value.trim()) { eis.classList.add('error-field'); rnt.classList.add('error-field'); error.classList.add('active'); error.setAttribute('role', 'alert'); return false; } else { eis.classList.remove('error-field'); rnt.classList.remove('error-field'); error.classList.remove('active'); return true; } } else { eis.classList.remove('error-field'); rnt.classList.remove('error-field'); error.classList.remove('active'); return true; } } document.getElementById('eis_url').addEventListener('input', function() { validateEISorRNT(); updateProgress(); }); document.getElementById('rnt').addEventListener('input', function() { validateEISorRNT(); updateProgress(); }); const closedPurchaseFields = [ 'platform_name', 'contract_number', 'customer_name', 'customer_inn', 'contract_subject', 'contract_url' ]; document.querySelectorAll('input[name=closed_purchase]').forEach(el = { el.addEventListener('change', function() { const isClosed = this.value === 'да'; document.getElementById('closed_purchase_block').classList.toggle('hidden', !isClosed); closedPurchaseFields.forEach(id = { const field = document.querySelector(`[name=${id}]`); if (field) { if (isClosed) { field.setAttribute('required', 'required'); field.setAttribute('aria-required', 'true'); } else { field.removeAttribute('required'); field.removeAttribute('aria-required'); field.value = ''; } } }); const urlField = document.querySelector('[name=contract_url]'); if (urlField) urlField.removeAttribute('required'); updateProgress(); }); }); function validateClosedInn() { const input = document.getElementById('customer_inn'); const error = document.getElementById('closed_inn_error'); let val = input.value.replace(Dg, ''); input.value = val; if (!document.getElementById('closed_purchase_block').classList.contains('hidden')) { if (val.length !== 10) { input.classList.add('error-field'); error.classList.add('active'); error.setAttribute('role', 'alert'); return false; } } input.classList.remove('error-field'); error.classList.remove('active'); return true; } document.getElementById('customer_inn').addEventListener('input', function() { validateClosedInn(); updateProgress(); }); document.querySelectorAll('input[name=reinsurance]').forEach(el = { el.addEventListener('change', function() { const bankBlock = document.getElementById('bank_name_block'); const bankInput = document.getElementById('bank_name_input'); if(this.value === 'другой банк') { bankBlock.classList.remove('hidden'); bankInput.setAttribute('required', 'required'); bankInput.setAttribute('aria-required', 'true'); } else { bankBlock.classList.add('hidden'); bankInput.removeAttribute('required'); bankInput.removeAttribute('aria-required'); bankInput.value = ''; document.getElementById('bank_name_error').classList.remove('active'); bankInput.classList.remove('error-field'); } updateProgress(); }); }); function validateBankName() { const bankInput = document.getElementById('bank_name_input'); const error = document.getElementById('bank_name_error'); if (!document.getElementById('bank_name_block').classList.contains('hidden')) { if (!bankInput.value.trim()) { bankInput.classList.add('error-field'); error.classList.add('active'); error.setAttribute('role', 'alert'); return false; } } bankInput.classList.remove('error-field'); error.classList.remove('active'); return true; } document.getElementById('bank_name_input').addEventListener('input', function() { validateBankName(); updateProgress(); }); --- Переходы между шагами --- toStep2.onclick = function() { if (!validateInn()) { orgInn.focus(); showNotification('Пожалуйста, введите корректный ИНН', 'error'); return; } if (!orgName.value.trim()) { showNotification('Пожалуйста, загрузите данные организации по ИНН', 'error'); dadataBtn.focus(); return; } if (!step1.checkValidity()) { step1.reportValidity(); return; } if (fzType.value === 'Коммерческая закупка') { if (!validateCommercialFields()) return; } if (fzType.value !== 'Коммерческая закупка') { if (!validateEISorRNT()) return; } if (!document.getElementById('closed_purchase_block').classList.contains('hidden')) { if (!validateClosedInn()) { document.getElementById('customer_inn').focus(); return; } } if (!validateBankName()) { document.getElementById('bank_name_input').focus(); return; } showStep(2); showNotification('Переходим ко второму шагу', 'info'); }; toStep1.onclick = function() { showStep(1); showNotification('Возвращаемся к первому шагу', 'info'); }; --- Блокировка поля Предложенная цена --- const offerPriceInput = document.getElementById('offer_price'); const noTradingCheckbox = document.getElementById('no_trading_checkbox'); if (noTradingCheckbox) { noTradingCheckbox.addEventListener('change', function() { offerPriceInput.disabled = this.checked; if (this.checked) { offerPriceInput.value = ''; offerPriceInput.removeAttribute('required'); offerPriceInput.removeAttribute('aria-required'); } else { offerPriceInput.setAttribute('required', 'required'); offerPriceInput.setAttribute('aria-required', 'true'); } updateProgress(); }); } --- Поле Есть ли аванс --- const hasAdvanceRadios = document.querySelectorAll('input[name=has_advance]'); const advanceBlock = document.getElementById('advance_block'); const advanceAmountInput = document.getElementById('advance_amount'); const advancePercentHint = document.getElementById('advance_percent_hint'); const nmckInput = document.getElementById('nmck'); hasAdvanceRadios.forEach(radio = { radio.addEventListener('change', function() { const isYes = this.value === 'да'; advanceBlock.classList.toggle('hidden', !isYes); if (isYes) { advanceAmountInput.setAttribute('required', 'required'); advanceAmountInput.setAttribute('aria-required', 'true'); } else { advanceAmountInput.removeAttribute('required'); advanceAmountInput.removeAttribute('aria-required'); advanceAmountInput.value = ''; advancePercentHint.textContent = ''; } updateProgress(); }); }); function calculateAdvancePercent() { const advanceAmount = parseFloat(parseFormattedAmount(advanceAmountInput.value)); const nmckValue = parseFloat(parseFormattedAmount(nmckInput.value)); if (isNaN(advanceAmount) advanceAmount = 0) { advancePercentHint.textContent = ''; return; } if (isNaN(nmckValue) nmckValue = 0) { advancePercentHint.textContent = 'Введите НМЦК для расчета процента аванса'; advancePercentHint.style.color = '#f59e0b'; return; } if (advanceAmount nmckValue) { advancePercentHint.textContent = 'Аванс не может превышать НМЦК!'; advancePercentHint.style.color = '#ef4444'; return; } const percent = (advanceAmount nmckValue) 100; advancePercentHint.textContent = `Аванс составляет ${percent.toFixed(2)}% от НМЦК`; advancePercentHint.style.color = '#10a37f'; } advanceAmountInput.addEventListener('input', function() { calculateAdvancePercent(); updateProgress(); }); nmckInput.addEventListener('input', function() { if (advanceAmountInput.value && !advanceBlock.classList.contains('hidden')) { calculateAdvancePercent(); } updateProgress(); }); --- ИСПРАВЛЕННЫЙ Подсчёт дней между датами --- const bgStartInput = document.getElementById('bg_start_date'); const bgEndInput = document.getElementById('bg_date'); const daysDiffElem = document.getElementById('days_diff'); Убираем валидацию при вводе дат - позволяем пользователю вводить любые даты function updateDaysDiff() { daysDiffElem.textContent = ''; daysDiffElem.setAttribute('aria-label', ''); if (!bgStartInput.value !bgEndInput.value) return; const start = new Date(bgStartInput.value); const end = new Date(bgEndInput.value); Сбрасываем время для корректного сравнения start.setHours(0, 0, 0, 0); end.setHours(0, 0, 0, 0); const diff = Math.round((end - start) (1000 60 60 24)); if (Number.isFinite(diff) && diff = 0) { daysDiffElem.textContent = diff + ' дн.'; daysDiffElem.setAttribute('aria-label', `${diff} дней между датами`); } else if (diff 0) { daysDiffElem.textContent = Math.abs(diff) + ' дн. назад'; daysDiffElem.setAttribute('aria-label', `${Math.abs(diff)} дней назад`); daysDiffElem.style.color = '#ef4444'; } else { daysDiffElem.textContent = ''; } } Разрешаем копирование и вставку в поля дат function allowCopyPasteDateInputs() { [bgStartInput, bgEndInput].forEach(input = { Разрешаем вставку input.addEventListener('paste', function(e) { e.preventDefault(); const text = (e.clipboardData window.clipboardData).getData('text'); Пытаемся распарсить дату из разных форматов let date = null; Формат DD.MM.YYYY const matchDMY = text.match((d{2}).(d{2}).(d{4})); if (matchDMY) { date = `${matchDMY[3]}-${matchDMY[2]}-${matchDMY[1]}`; } Формат YYYY-MM-DD const matchISO = text.match((d{4})-(d{2})-(d{2})); if (matchISO) { date = text.substring(0, 10); } if (date) { this.value = date; this.dispatchEvent(new Event('input', { bubbles true })); } }); Разрешаем ручной ввод input.addEventListener('input', function() { updateDaysDiff(); updateProgress(); }); Убираем проверку на корректность дат при вводе input.addEventListener('change', function() { updateDaysDiff(); updateProgress(); }); }); } Валидация дат только при отправке формы function validateDates() { if (!bgStartInput.value !bgEndInput.value) { return { valid false, message 'Заполните обе даты' }; } const start = new Date(bgStartInput.value); const end = new Date(bgEndInput.value); start.setHours(0, 0, 0, 0); end.setHours(0, 0, 0, 0); if (start = end) { return { valid false, message 'Дата окончания должна быть позже даты начала' }; } return { valid true }; } bgStartInput.addEventListener('input', function() { updateDaysDiff(); updateProgress(); }); bgEndInput.addEventListener('input', function() { updateDaysDiff(); updateProgress(); }); Инициализация разрешения копированиявставки allowCopyPasteDateInputs(); --- Экспорт в CSV (Excel) --- function exportToCSV() { const formData = {}; function getFieldValue(field) { if (field.type === 'radio') { const checked = document.querySelector(`[name=${field.name}]checked`); return checked checked.value ''; } else if (field.type === 'checkbox') { return field.checked 'Да' 'Нет'; } else { return field.value ''; } } const allFields = {}; document.querySelectorAll('input, select, textarea').forEach(field = { if (field.name && !allFields[field.name]) { allFields[field.name] = field; } }); const csvData = []; csvData.push(['Поле', 'Значение']); Object.values(allFields).forEach(field = { const labelElement = document.querySelector(`label[for=${field.id}]`) field.closest('.form-group').querySelector('label'); let label = 'Неизвестное поле'; if (labelElement) { label = labelElement.textContent .replace(span.spang, '') .replace(s+g, ' ') .trim(); } const value = getFieldValue(field); if (label !== 'Неизвестное поле' && value !== '') { csvData.push([label, value]); } }); const daysDiff = daysDiffElem.textContent; if (daysDiff) { csvData.push(['Срок БГ (дней)', daysDiff.replace(' дн.', '').replace(' дн. назад', '')]); } if (advanceAmountInput.value && nmckInput.value) { const advanceAmount = parseFloat(parseFormattedAmount(advanceAmountInput.value)); const nmckValue = parseFloat(parseFormattedAmount(nmckInput.value)); if (!isNaN(advanceAmount) && !isNaN(nmckValue) && nmckValue 0) { const percent = (advanceAmount nmckValue) 100; csvData.push(['Аванс (%)', percent.toFixed(2) + '%']); } } const csvContent = csvData.map(row = row.map(cell = { const escaped = String(cell).replace(g, ''); if (escaped.includes(',') escaped.includes('') escaped.includes('n') escaped.includes(';')) { return `${escaped}`; } return escaped; }).join(';') ).join('n'); const blob = new Blob(['uFEFF' + csvContent], { type 'textcsv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.href = url; const orgName = (document.querySelector('[name=org_name]').value 'организация') .replace([[]]g, '') .replace(s+g, '_') .substring(0, 30); const inn = (document.querySelector('[name=org_inn]').value ''); const date = new Date().toISOString().split('T')[0]; link.download = `Скоринг_${orgName}_${inn}_${date}.csv`; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(() = URL.revokeObjectURL(url), 100); showNotification('Файл CSV успешно экспортирован', 'success'); } document.getElementById('export_csv').addEventListener('click', function() { if (step2.checkValidity()) { exportToCSV(); } else { showNotification('Заполните все обязательные поля перед экспортом', 'warning'); } }); --- ИСПРАВЛЕННАЯ Генерация PDF с корректными ссылками --- function formatDateDMY(dateStr) { if (!dateStr) return ''; const [yyyy, mm, dd] = dateStr.split('-'); if (!yyyy !mm !dd) return dateStr; return `${dd}.${mm}.${yyyy}`; } Функция форматирования чисел с Руб. вместо знака рубля function formatNumberWithRub(num) { if (!num) return '0 Руб.'; const number = parseFloat(num); if (isNaN(number)) return '0 Руб.'; Форматируем с пробелами const formatted = number.toLocaleString('ru-RU', { minimumFractionDigits 2, maximumFractionDigits 2 }).replace(,g, ','); return formatted + ' Руб.'; } Функция для создания ссылки как на скрине (простой текст без разрывов) function createLinkText(url) { if (!url) return ''; Убираем https для отображения (но сохраняем полный URL для клика) const displayText = url.replace(^https, ''); Возвращаем как обычный текст без разрывов return displayText; } document.getElementById('submit_form').addEventListener('click', async function(e) { e.preventDefault(); Проверка валидации формы if (!step2.checkValidity()) { step2.reportValidity(); return; } Проверка ИНН if (!validateInn()) { showNotification('Пожалуйста, введите корректный ИНН', 'error'); return; } Проверка наименования организации if (!orgName.value.trim()) { showNotification('Пожалуйста, загрузите данные организации по ИНН', 'error'); return; } Проверка дат const dateValidation = validateDates(); if (!dateValidation.valid) { showNotification(dateValidation.message, 'error'); bgEndInput.focus(); return; } Проверка аванса const hasAdvance = document.querySelector('input[name=has_advance]checked'); if (hasAdvance && hasAdvance.value === 'да') { const advanceAmount = parseFloat(parseFormattedAmount(advanceAmountInput.value)); const nmckValue = parseFloat(parseFormattedAmount(nmckInput.value)); if (isNaN(advanceAmount) advanceAmount = 0) { showNotification('Пожалуйста, введите сумму аванса', 'error'); advanceAmountInput.focus(); return; } if (!isNaN(nmckValue) && advanceAmount nmckValue) { showNotification('Аванс не может превышать НМЦК!', 'error'); advanceAmountInput.focus(); return; } } Получение значений полей const getVal = name = document.querySelector(`[name=${name}]`).value ''; const getRadio = name = (document.querySelector(`[name=${name}]checked`) {}).value ''; const closedPurchase = getRadio('closed_purchase') === 'да'; const reinsurance = getRadio('reinsurance'); const hasAdvanceValue = getRadio('has_advance') === 'да'; const isCommercial = fzType.value === 'Коммерческая закупка'; Расчет дней БГ let daysBG = ''; const bgStart = getVal('bg_start_date'); const bgEnd = getVal('bg_date'); if(bgStart && bgEnd) { const d1 = new Date(bgStart); const d2 = new Date(bgEnd); d1.setHours(0,0,0,0); d2.setHours(0,0,0,0); const diff = Math.round((d2 - d1) (1000 60 60 24)); if (Number.isFinite(diff) && diff = 0) daysBG = diff; } Расчет процента аванса let advancePercent = ''; if (hasAdvanceValue) { const advanceAmount = parseFloat(parseFormattedAmount(getVal('advance_amount'))); const nmckValue = parseFloat(parseFormattedAmount(getVal('nmck'))); if (!isNaN(advanceAmount) && !isNaN(nmckValue) && nmckValue 0) { advancePercent = ((advanceAmount nmckValue) 100).toFixed(2); } } Цвета для разделов PDF const sectionColors = { main '#e0f2fe', Голубой для основной информации finance '#f0f9ff', Светло-голубой для финансовых данных dates '#fefce8', Желтый для сроков comment '#f1f5f9' Серый для комментария }; Создание HTML для PDF с простыми ссылками как на скрине let pdfHTML = ` div style=font-family'Inter','Arial',sans-serif;background#ffffff;color#1e293b;width750px;padding40px;line-height1.5; div style=font-size1.5rem;font-weight600;margin-bottom4px;color#1e293b;${getVal('org_name') '—'}div div style=font-size1rem;margin-bottom32px;color#475569;ИНН ${getVal('org_inn') '—'}div !-- Основная информация -- div style=background${sectionColors.main};border-radius12px;padding24px;margin-bottom24px;border-left4px solid #0ea5e9; div style=font-size1rem;font-weight600;margin-bottom16px;color#0c4a6e;Основная информацияdiv `; Добавляем информацию в зависимости от типа закупки if (isCommercial) { pdfHTML += ` div style=margin-bottom8px;bНаименование площадкиb ${getVal('commercial_platform_name')}div div style=margin-bottom8px;bНомер контрактадоговораb ${getVal('commercial_contract_number')}div div style=margin-bottom8px;bНаименование заказчика (Бенефициар)b ${getVal('commercial_customer_name')}div div style=margin-bottom8px;bИНН заказчикаb ${getVal('commercial_customer_inn')}div div style=margin-bottom8px;bПредмет контрактадоговораb ${getVal('commercial_contract_subject')}div `; const commercialContractUrl = getVal('commercial_contract_url'); if (commercialContractUrl) { const linkText = createLinkText(commercialContractUrl); pdfHTML += `div style=margin-bottom8px;bСсылка на контрактдоговорb span style=color#1e293b;font-family'Inter',sans-serif;${linkText}spandiv`; } } else { const eisUrl = getVal('eis_url'); if (eisUrl) { const linkText = createLinkText(eisUrl); pdfHTML += `div style=margin-bottom8px;bСсылка на ЕИСаукционb span style=color#1e293b;font-family'Inter',sans-serif;${linkText}spandiv`; } const rnt = getVal('rnt'); if (rnt) { pdfHTML += `div style=margin-bottom8px;bРНТb ${rnt}div`; } } Продолжение основной информации pdfHTML += ` div style=margin-bottom8px;bВид ФЗb ${getVal('fz_type')}div div style=margin-bottom8px;bВид БГb ${getVal('bg_type')}div div style=margin-bottom8px;bЗакрытая закупкаb ${closedPurchase 'да' 'нет'}div `; Если закрытая закупка (и не коммерческая) if (!isCommercial && closedPurchase) { pdfHTML += ` div style=margin-bottom8px;bНаименование площадкиb ${getVal('platform_name')}div div style=margin-bottom8px;bНомер контрактадоговораb ${getVal('contract_number')}div div style=margin-bottom8px;bНаименование заказчика (Бенефициар)b ${getVal('customer_name')}div div style=margin-bottom8px;bИНН заказчикаb ${getVal('customer_inn')}div div style=margin-bottom8px;bПредмет контрактадоговораb ${getVal('contract_subject')}div `; const contractUrl = getVal('contract_url'); if (contractUrl) { const linkText = createLinkText(contractUrl); pdfHTML += `div style=margin-bottom8px;bСсылка на контрактдоговорb span style=color#1e293b;font-family'Inter',sans-serif;${linkText}spandiv`; } } pdfHTML += ` div style=margin-bottom8px;bЗакупка у единственного поставщикаb ${getRadio('single_supplier') '—'}div div style=margin-bottom8px;bПереобеспечениеb ${reinsurance '—'}div `; if (reinsurance === 'другой банк') { pdfHTML += `div style=margin-bottom8px;bБанкb ${getVal('bank_name')}div`; } pdfHTML += ` div !-- Финансовые данные -- div style=background${sectionColors.finance};border-radius12px;padding24px;margin-bottom24px;border-left4px solid #3b82f6; div style=font-size1rem;font-weight600;margin-bottom16px;color#1e40af;Финансовые данныеdiv div style=margin-bottom8px;bНМЦКb ${formatNumberWithRub(parseFormattedAmount(getVal('nmck')))}div div style=margin-bottom8px;bПредложенная ценаb ${getVal('offer_price') formatNumberWithRub(parseFormattedAmount(getVal('offer_price'))) (document.getElementById('no_trading_checkbox').checked 'Торги не проведены' '—')}div div style=margin-bottom8px;bСумма БГb ${formatNumberWithRub(parseFormattedAmount(getVal('bg_amount')))}div div style=margin-bottom8px;bЕсть ли авансb ${hasAdvanceValue 'да' 'нет'}div `; if (hasAdvanceValue) { pdfHTML += ` div style=margin-bottom8px;bСумма авансаb ${formatNumberWithRub(parseFormattedAmount(getVal('advance_amount')))}div `; if (advancePercent) { pdfHTML += `div style=margin-bottom8px;bАванс в % от НМЦКb ${advancePercent}%div`; } } pdfHTML += ` div !-- Сроки -- div style=background${sectionColors.dates};border-radius12px;padding24px;margin-bottom24px;border-left4px solid #f59e0b; div style=font-size1rem;font-weight600;margin-bottom16px;color#92400e;Срокиdiv div style=margin-bottom8px;bНачало действия БГb ${formatDateDMY(getVal('bg_start_date')) '—'}div div style=margin-bottom8px;bОкончание действия БГb ${formatDateDMY(getVal('bg_date')) '—'} ${(daysBG && daysBG = 0) ` (${daysBG} дн.)` ''}div div `; const comment = getVal('comment'); if (comment) { pdfHTML += ` !-- Комментарий -- div style=background${sectionColors.comment};border-radius12px;padding24px;margin-bottom24px;border-left4px solid #64748b; div style=font-size1rem;font-weight600;margin-bottom16px;color#334155;Комментарийdiv div style=font-size0.875rem;line-height1.5;color#475569;white-space pre-wrap;${comment}div div `; } pdfHTML += ` div style=color#64748b;font-size0.75rem;text-aligncenter;margin-top32px;padding-top16px;border-top1px solid #e2e8f0; Сгенерировано ${new Date().toLocaleDateString('ru-RU')} ${new Date().toLocaleTimeString('ru-RU', {hour '2-digit', minute'2-digit'})} div div `; Генерация имени файла let orgNameVal = (getVal('org_name') '').replace([[]]g, '').replace(s+g, '_'); let inn = (getVal('org_inn') '').replace(Dg, ''); let bgAmount = (getVal('bg_amount') '').replace(sg, ''); let parts = []; if (orgNameVal) parts.push(orgNameVal); if (inn) parts.push(inn); if (bgAmount) parts.push(bgAmount); let fileName = parts.join('_') + '.pdf'; Генерация PDF const pdfBlock = document.getElementById('pdf-preview'); pdfBlock.innerHTML = pdfHTML; try { const canvas = await html2canvas(pdfBlock, { scale 2, useCORS true, allowTaint true, backgroundColor '#ffffff' }); const imgData = canvas.toDataURL('imagepng'); let pdf = new window.jspdf.jsPDF({ orientation 'p', unit 'pt', format 'a4' }); const pageWidth = pdf.internal.pageSize.getWidth(); const imgWidth = pageWidth - 40; const imgHeight = canvas.height imgWidth canvas.width; pdf.addImage(imgData, 'PNG', 20, 20, imgWidth, imgHeight, '', 'FAST'); pdf.save(fileName); showNotification('PDF файл успешно сгенерирован', 'success'); } catch (error) { console.error('Ошибка генерации PDF', error); showNotification('Ошибка при генерации PDF', 'error'); } }); --- Инициализация при загрузке --- document.addEventListener('DOMContentLoaded', function() { Устанавливаем даты по умолчанию const today = new Date(); const todayStr = today.toISOString().split('T')[0]; const futureDate = new Date(); futureDate.setDate(futureDate.getDate() + 30); const futureDateStr = futureDate.toISOString().split('T')[0]; bgStartInput.value = todayStr; bgEndInput.value = futureDateStr; Настраиваем поля с суммами setupAmountInputs(); Обновляем прогресс и счетчик дней updateDaysDiff(); updateProgress(); updateDadataButton(); Назначаем обработчики событий для обновления прогресса document.querySelectorAll('input, select, textarea').forEach(field = { field.addEventListener('input', updateProgress); field.addEventListener('change', updateProgress); }); document.querySelectorAll('.checkbox-option input[type=radio]').forEach(radio = { radio.addEventListener('change', updateProgress); }); showNotification('Форма готова. API DaData подключен.', 'success'); }); script body html
ИП Копалкин Алексей Александрович
ИНН 500406872158 | ОГРНИП 321508100374340 © 2021
НАВИГАЦИЯ
  • Главная
  • Этапы работы
О НАС
  • Гарантии
  • Преимущества
ПОМОЩЬ
  • Контакты
  • Заказать обратный звонок
  • Виды предоставляемых услуг