!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