Направление:
Объем перевода (в страницах)
Предварительная стоимость:
Документ можно прислать позже — менеджер свяжется с вами.
Комментарий
Нажимая кнопку, вы соглашаетесь с
политикой конфиденциальности .
Заявка успешно отправлена!
В ближайшее время с вами свяжется наш менеджер для уточнения деталей заказа.
Хорошо
// в названии услуги или языка разорвала бы тег и открыла инъекцию.
const pricesData = {"description":"Тарифы на переводы. Цены указаны за одну переводческую страницу (1800 знаков с пробелами)","currency":"RUB","currency_symbol":"₽","unit":"страница","unit_size":"1800 знаков с пробелами","min_order":1500,"languages":[{"id":"ru","name":"Русский","flag":"🇷🇺","group":0,"price_from_foreign":0,"price_to_foreign":0},{"id":"en","name":"Английский","flag":"🇺🇸","group":1,"price_from_foreign":600,"price_to_foreign":650},{"id":"de","name":"Немецкий","flag":"🇩🇪","group":1,"price_from_foreign":600,"price_to_foreign":650},{"id":"uk","name":"Украинский","flag":"🇺🇦","group":5,"price_from_foreign":550,"price_to_foreign":600},{"id":"be","name":"Белорусский","flag":"🇧🇾","group":5,"price_from_foreign":550,"price_to_foreign":600},{"id":"fr","name":"Французский","flag":"🇫🇷","group":2,"price_from_foreign":850,"price_to_foreign":900},{"id":"es","name":"Испанский","flag":"🇪🇸","group":2,"price_from_foreign":850,"price_to_foreign":900},{"id":"it","name":"Итальянский","flag":"🇮🇹","group":2,"price_from_foreign":850,"price_to_foreign":900},{"id":"pl","name":"Польский","flag":"🇵🇱","group":2,"price_from_foreign":850,"price_to_foreign":900},{"id":"el","name":"Греческий","flag":"🇬🇷","group":3,"price_from_foreign":850,"price_to_foreign":900},{"id":"pt","name":"Португальский","flag":"🇵🇹","group":3,"price_from_foreign":850,"price_to_foreign":900},{"id":"hu","name":"Венгерский","flag":"🇭🇺","group":3,"price_from_foreign":850,"price_to_foreign":900},{"id":"mo","name":"Молдавский","flag":"🇲🇩","group":3,"price_from_foreign":850,"price_to_foreign":900},{"id":"tk","name":"Туркменский","flag":"🇹🇲","group":3,"price_from_foreign":850,"price_to_foreign":900},{"id":"cs","name":"Чешский","flag":"🇨🇿","group":3,"price_from_foreign":850,"price_to_foreign":900},{"id":"ka","name":"Грузинский","flag":"🇬🇪","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"bg","name":"Болгарский","flag":"🇧🇬","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"lv","name":"Латышский","flag":"🇱🇻","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"lt","name":"Литовский","flag":"🇱🇹","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"no","name":"Норвежский","flag":"🇳🇴","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"nl","name":"Нидерландский","flag":"🇳🇱","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"sv","name":"Шведский","flag":"🇸🇪","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"ro","name":"Румынский","flag":"🇷🇴","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"sr","name":"Сербский","flag":"🇷🇸","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"sk","name":"Словацкий","flag":"🇸🇰","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"sl","name":"Словенский","flag":"🇸🇮","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"fi","name":"Финский","flag":"🇫🇮","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"az","name":"Азербайджанский","flag":"🇦🇿","group":6,"price_from_foreign":950,"price_to_foreign":1000},{"id":"hy","name":"Армянский","flag":"🇦🇲","group":6,"price_from_foreign":950,"price_to_foreign":1000},{"id":"da","name":"Датский","flag":"🇩🇰","group":6,"price_from_foreign":950,"price_to_foreign":1000},{"id":"kk","name":"Казахский","flag":"🇰🇿","group":6,"price_from_foreign":950,"price_to_foreign":1000},{"id":"ky","name":"Киргизский","flag":"🇰🇬","group":6,"price_from_foreign":950,"price_to_foreign":1000},{"id":"tg","name":"Таджикский","flag":"🇹🇯","group":6,"price_from_foreign":950,"price_to_foreign":1000},{"id":"tr","name":"Турецкий","flag":"🇹🇷","group":6,"price_from_foreign":950,"price_to_foreign":1000},{"id":"hr","name":"Хорватский","flag":"🇭🇷","group":6,"price_from_foreign":950,"price_to_foreign":1000},{"id":"uz","name":"Узбекский","flag":"🇺🇿","group":6,"price_from_foreign":950,"price_to_foreign":1000},{"id":"ar","name":"Арабский","flag":"🇸🇦","group":7,"price_from_foreign":1200,"price_to_foreign":1250},{"id":"vi","name":"Вьетнамский","flag":"🇻🇳","group":7,"price_from_foreign":1200,"price_to_foreign":1250},{"id":"he","name":"Иврит","flag":"🇮🇱","group":7,"price_from_foreign":1200,"price_to_foreign":1250},{"id":"id","name":"Индонезийский","flag":"🇮🇩","group":7,"price_from_foreign":1200,"price_to_foreign":1250},{"id":"th","name":"Тайский","flag":"🇹🇭","group":7,"price_from_foreign":1200,"price_to_foreign":1250},{"id":"zh","name":"Китайский","flag":"🇨🇳","group":7,"price_from_foreign":1200,"price_to_foreign":1250},{"id":"ko","name":"Корейский","flag":"🇰🇷","group":7,"price_from_foreign":1200,"price_to_foreign":1250},{"id":"fa","name":"Персидский","flag":"🇮🇷","group":7,"price_from_foreign":1200,"price_to_foreign":1250},{"id":"hi","name":"Хинди","flag":"🇮🇳","group":7,"price_from_foreign":1200,"price_to_foreign":1250},{"id":"ja","name":"Японский","flag":"🇯🇵","group":7,"price_from_foreign":1200,"price_to_foreign":1250},{"id":"et","name":"Эстонский","flag":"🇪🇪","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"sq","name":"Албанский","flag":"🇦🇱","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"mn","name":"Монгольский","flag":"🇲🇳","group":4,"price_from_foreign":950,"price_to_foreign":1000},{"id":"ms","name":"Малайский","flag":"🇲🇾","group":7,"price_from_foreign":1200,"price_to_foreign":1250},{"id":"ca","name":"Каталанский","flag":"🇪🇸","group":4,"price_from_foreign":950,"price_to_foreign":1000}],"notarial_services":[{"id":"notarial_translation","name":"Нотариальное заверение перевода","price":1200,"unit":"документ"},{"id":"notarial_copy","name":"Нотариальное заверение копии документа","price":350,"unit":"страница"},{"id":"identity_certification","name":"Удостоверение тождественности документа","price":800,"unit":"страница"},{"id":"translation_plus_identity","name":"Перевод + удостоверение тождественности","price":3000,"unit":"комплект"},{"id":"bureau_stamp","name":"Заверение печатью бюро","price":300,"unit":"документ"},{"id":"passport_notarial","name":"Перевод паспорта с нотариальным заверением","price":2500,"unit":"документ"},{"id":"passport_notarial_urgent","name":"Перевод паспорта с нотариальным заверением (срочно, день в день)","price":4000,"unit":"документ"},{"id":"consular_legalization","name":"Консульская легализация (МИД + консульство)","price":10000,"unit":"документ"},{"id":"tpp_certification","name":"Заверение в ТПП РФ","price":5000,"unit":"документ"},{"id":"mid_legalization","name":"Легализация в МИД РФ","price":5000,"unit":"документ"}],"oral_translation":[{"id":"consecutive_en_de","name":"Последовательный перевод (английский, немецкий)","price":5000,"unit":"час","min_hours":3},{"id":"consecutive_es_it_fr","name":"Последовательный перевод (испанский, итальянский, французский)","price":7000,"unit":"час","min_hours":3},{"id":"consecutive_zh_ar","name":"Последовательный перевод (китайский, арабский)","price":8000,"unit":"час","min_hours":3},{"id":"simultaneous","name":"Синхронный перевод","price":10000,"unit":"час","min_hours":4,"price_note":"от"}],"oral_online_discount":2000,"oral_per_person_discount":1000,"additional_services":[{"id":"translator_signature","name":"Заверение подписи переводчика + печать бюро переводов","price":300,"unit":"документ","description":"Заверение подписи переводчика и печать бюро переводов"},{"id":"notarial","name":"Нотариальное заверение","price":1200,"unit":"документ","description":"Заверение перевода у нотариуса"},{"id":"apostille","name":"Апостиль","price":5000,"unit":"документ","description":"Проставление апостиля, срок 7 дней"},{"id":"layout","name":"Верстка документа (сложная)","price":500,"unit":"полоса","description":"Сохранение форматирования, 1-2 дня"},{"id":"audio_video","name":"Перевод аудио\/видео","price":500,"unit":"минута","description":"Перевод аудио- и видеоматериалов"},{"id":"delivery_moscow","name":"Курьерская доставка (в пределах МКАД)","price":0,"unit":"заказ","description":"Бесплатная доставка в день готовности"}],"discounts":[{"from_pages":50,"to_pages":80,"percent":3},{"from_pages":80,"to_pages":100,"percent":5},{"from_pages":100,"to_pages":150,"percent":7},{"from_pages":150,"to_pages":null,"percent":10}],"notarial_bulk_discount":{"from_documents":8,"price_per_document":1000,"note":"При заверении от 8 документов"},"government_fees":{"apostille_gosposhlina":2500},"notes":["Цены действительны на текущий период","Минимальный заказ — 1 500 ₽","Точный расчет после анализа документа","Для постоянных клиентов действуют скидки","Цены могут отличаться для редких языков"]};
const minOrder = pricesData.min_order || 1500;
// i18n строки для калькулятора
const calcI18nUI = {
submitText: '\u041E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C\u0020\u0437\u0430\u044F\u0432\u043A\u0443',
sending: '\u041E\u0442\u043F\u0440\u0430\u0432\u043A\u0430...',
errorGeneric: '\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430\u0020\u043E\u0448\u0438\u0431\u043A\u0430\u0020\u043F\u0440\u0438\u0020\u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0435.\u0020\u041F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435\u0020\u043F\u043E\u0437\u0436\u0435\u0020\u0438\u043B\u0438\u0020\u043F\u043E\u0437\u0432\u043E\u043D\u0438\u0442\u0435\u0020\u043D\u0430\u043C.',
errorNetwork: '\u041E\u0448\u0438\u0431\u043A\u0430\u0020\u0441\u0435\u0442\u0438.\u0020\u041F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435\u0020\u0438\u043D\u0442\u0435\u0440\u043D\u0435\u0442\u002D\u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435\u0020\u0438\u0020\u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435\u0020\u0441\u043D\u043E\u0432\u0430.',
fileRequired: '',
quoteLeadSubmit: "Отправить",
quoteLeadInvalidEmail: "Укажите корректный email",
rejectedTitle: "Заявка принята, но часть файлов не загрузилась (%d):",
rejectedCta: "Пришлите их повторно в другом формате (PDF, DOCX, JPG) или на %s — менеджер уже видит вашу заявку.",
tooManyFiles: "Можно прикрепить не более %d файлов за раз. Остальные пришлите отдельным сообщением или архивом (ZIP).",
};
const calcI18n = {
pages: '\u0441\u0442\u0440.',
times: '\u00D7',
discount: '\u0421\u043A\u0438\u0434\u043A\u0430',
docs: '\u0434\u043E\u043A.',
addons: {
'translator_signature': '\u0417\u0430\u0432\u0435\u0440\u0435\u043D\u0438\u0435\u0020\u043F\u043E\u0434\u043F\u0438\u0441\u0438\u0020\u043F\u0435\u0440\u0435\u0432\u043E\u0434\u0447\u0438\u043A\u0430\u0020\u002B\u0020\u043F\u0435\u0447\u0430\u0442\u044C\u0020\u0431\u044E\u0440\u043E',
'notarial': '\u041D\u043E\u0442\u0430\u0440\u0438\u0430\u043B\u044C\u043D\u043E\u0435\u0020\u0437\u0430\u0432\u0435\u0440\u0435\u043D\u0438\u0435',
'apostille': '\u0410\u043F\u043E\u0441\u0442\u0438\u043B\u044C',
'layout': '\u0412\u0435\u0440\u0441\u0442\u043A\u0430\u0020\u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0020\u0028\u0441\u043B\u043E\u0436\u043D\u0430\u044F\u0029',
'audio_video': '\u041F\u0435\u0440\u0435\u0432\u043E\u0434\u0020\u0430\u0443\u0434\u0438\u043E\/\u0432\u0438\u0434\u0435\u043E',
'delivery_moscow': 'calculator.addon_delivery_moscow'
},
langNames: {
'ru': '\u0420\u0443\u0441\u0441\u043A\u0438\u0439',
'en': '\u0410\u043D\u0433\u043B\u0438\u0439\u0441\u043A\u0438\u0439',
'de': '\u041D\u0435\u043C\u0435\u0446\u043A\u0438\u0439',
'uk': '\u0423\u043A\u0440\u0430\u0438\u043D\u0441\u043A\u0438\u0439',
'be': '\u0411\u0435\u043B\u043E\u0440\u0443\u0441\u0441\u043A\u0438\u0439',
'fr': '\u0424\u0440\u0430\u043D\u0446\u0443\u0437\u0441\u043A\u0438\u0439',
'es': '\u0418\u0441\u043F\u0430\u043D\u0441\u043A\u0438\u0439',
'it': '\u0418\u0442\u0430\u043B\u044C\u044F\u043D\u0441\u043A\u0438\u0439',
'pl': '\u041F\u043E\u043B\u044C\u0441\u043A\u0438\u0439',
'el': '\u0413\u0440\u0435\u0447\u0435\u0441\u043A\u0438\u0439',
'pt': '\u041F\u043E\u0440\u0442\u0443\u0433\u0430\u043B\u044C\u0441\u043A\u0438\u0439',
'hu': '\u0412\u0435\u043D\u0433\u0435\u0440\u0441\u043A\u0438\u0439',
'mo': '\u041C\u043E\u043B\u0434\u0430\u0432\u0441\u043A\u0438\u0439',
'tk': '\u0422\u0443\u0440\u043A\u043C\u0435\u043D\u0441\u043A\u0438\u0439',
'cs': '\u0427\u0435\u0448\u0441\u043A\u0438\u0439',
'ka': '\u0413\u0440\u0443\u0437\u0438\u043D\u0441\u043A\u0438\u0439',
'bg': '\u0411\u043E\u043B\u0433\u0430\u0440\u0441\u043A\u0438\u0439',
'lv': '\u041B\u0430\u0442\u044B\u0448\u0441\u043A\u0438\u0439',
'lt': '\u041B\u0438\u0442\u043E\u0432\u0441\u043A\u0438\u0439',
'no': '\u041D\u043E\u0440\u0432\u0435\u0436\u0441\u043A\u0438\u0439',
'nl': '\u041D\u0438\u0434\u0435\u0440\u043B\u0430\u043D\u0434\u0441\u043A\u0438\u0439',
'sv': '\u0428\u0432\u0435\u0434\u0441\u043A\u0438\u0439',
'ro': '\u0420\u0443\u043C\u044B\u043D\u0441\u043A\u0438\u0439',
'sr': '\u0421\u0435\u0440\u0431\u0441\u043A\u0438\u0439',
'sk': '\u0421\u043B\u043E\u0432\u0430\u0446\u043A\u0438\u0439',
'sl': '\u0421\u043B\u043E\u0432\u0435\u043D\u0441\u043A\u0438\u0439',
'fi': '\u0424\u0438\u043D\u0441\u043A\u0438\u0439',
'az': '\u0410\u0437\u0435\u0440\u0431\u0430\u0439\u0434\u0436\u0430\u043D\u0441\u043A\u0438\u0439',
'hy': '\u0410\u0440\u043C\u044F\u043D\u0441\u043A\u0438\u0439',
'da': '\u0414\u0430\u0442\u0441\u043A\u0438\u0439',
'kk': '\u041A\u0430\u0437\u0430\u0445\u0441\u043A\u0438\u0439',
'ky': '\u041A\u0438\u0440\u0433\u0438\u0437\u0441\u043A\u0438\u0439',
'tg': '\u0422\u0430\u0434\u0436\u0438\u043A\u0441\u043A\u0438\u0439',
'tr': '\u0422\u0443\u0440\u0435\u0446\u043A\u0438\u0439',
'hr': '\u0425\u043E\u0440\u0432\u0430\u0442\u0441\u043A\u0438\u0439',
'uz': '\u0423\u0437\u0431\u0435\u043A\u0441\u043A\u0438\u0439',
'ar': '\u0410\u0440\u0430\u0431\u0441\u043A\u0438\u0439',
'vi': '\u0412\u044C\u0435\u0442\u043D\u0430\u043C\u0441\u043A\u0438\u0439',
'he': '\u0418\u0432\u0440\u0438\u0442',
'id': '\u0418\u043D\u0434\u043E\u043D\u0435\u0437\u0438\u0439\u0441\u043A\u0438\u0439',
'th': '\u0422\u0430\u0439\u0441\u043A\u0438\u0439',
'zh': '\u041A\u0438\u0442\u0430\u0439\u0441\u043A\u0438\u0439',
'ko': '\u041A\u043E\u0440\u0435\u0439\u0441\u043A\u0438\u0439',
'fa': '\u041F\u0435\u0440\u0441\u0438\u0434\u0441\u043A\u0438\u0439',
'hi': '\u0425\u0438\u043D\u0434\u0438',
'ja': '\u042F\u043F\u043E\u043D\u0441\u043A\u0438\u0439',
'et': '\u042D\u0441\u0442\u043E\u043D\u0441\u043A\u0438\u0439',
'sq': '\u0410\u043B\u0431\u0430\u043D\u0441\u043A\u0438\u0439',
'mn': '\u041C\u043E\u043D\u0433\u043E\u043B\u044C\u0441\u043A\u0438\u0439',
'ms': '\u041C\u0430\u043B\u0430\u0439\u0441\u043A\u0438\u0439',
'ca': '\u041A\u0430\u0442\u0430\u043B\u0430\u043D\u0441\u043A\u0438\u0439'
}
};
// Маппинг флагов: id → emoji
const langFlags = {
'ru': '🇷🇺',
'en': '\uD83C\uDDFA\uD83C\uDDF8',
'de': '\uD83C\uDDE9\uD83C\uDDEA',
'uk': '\uD83C\uDDFA\uD83C\uDDE6',
'be': '\uD83C\uDDE7\uD83C\uDDFE',
'fr': '\uD83C\uDDEB\uD83C\uDDF7',
'es': '\uD83C\uDDEA\uD83C\uDDF8',
'it': '\uD83C\uDDEE\uD83C\uDDF9',
'pl': '\uD83C\uDDF5\uD83C\uDDF1',
'el': '\uD83C\uDDEC\uD83C\uDDF7',
'pt': '\uD83C\uDDF5\uD83C\uDDF9',
'hu': '\uD83C\uDDED\uD83C\uDDFA',
'mo': '\uD83C\uDDF2\uD83C\uDDE9',
'tk': '\uD83C\uDDF9\uD83C\uDDF2',
'cs': '\uD83C\uDDE8\uD83C\uDDFF',
'ka': '\uD83C\uDDEC\uD83C\uDDEA',
'bg': '\uD83C\uDDE7\uD83C\uDDEC',
'lv': '\uD83C\uDDF1\uD83C\uDDFB',
'lt': '\uD83C\uDDF1\uD83C\uDDF9',
'no': '\uD83C\uDDF3\uD83C\uDDF4',
'nl': '\uD83C\uDDF3\uD83C\uDDF1',
'sv': '\uD83C\uDDF8\uD83C\uDDEA',
'ro': '\uD83C\uDDF7\uD83C\uDDF4',
'sr': '\uD83C\uDDF7\uD83C\uDDF8',
'sk': '\uD83C\uDDF8\uD83C\uDDF0',
'sl': '\uD83C\uDDF8\uD83C\uDDEE',
'fi': '\uD83C\uDDEB\uD83C\uDDEE',
'az': '\uD83C\uDDE6\uD83C\uDDFF',
'hy': '\uD83C\uDDE6\uD83C\uDDF2',
'da': '\uD83C\uDDE9\uD83C\uDDF0',
'kk': '\uD83C\uDDF0\uD83C\uDDFF',
'ky': '\uD83C\uDDF0\uD83C\uDDEC',
'tg': '\uD83C\uDDF9\uD83C\uDDEF',
'tr': '\uD83C\uDDF9\uD83C\uDDF7',
'hr': '\uD83C\uDDED\uD83C\uDDF7',
'uz': '\uD83C\uDDFA\uD83C\uDDFF',
'ar': '\uD83C\uDDF8\uD83C\uDDE6',
'vi': '\uD83C\uDDFB\uD83C\uDDF3',
'he': '\uD83C\uDDEE\uD83C\uDDF1',
'id': '\uD83C\uDDEE\uD83C\uDDE9',
'th': '\uD83C\uDDF9\uD83C\uDDED',
'zh': '\uD83C\uDDE8\uD83C\uDDF3',
'ko': '\uD83C\uDDF0\uD83C\uDDF7',
'fa': '\uD83C\uDDEE\uD83C\uDDF7',
'hi': '\uD83C\uDDEE\uD83C\uDDF3',
'ja': '\uD83C\uDDEF\uD83C\uDDF5',
'et': '\uD83C\uDDEA\uD83C\uDDEA',
'sq': '\uD83C\uDDE6\uD83C\uDDF1',
'mn': '\uD83C\uDDF2\uD83C\uDDF3',
'ms': '\uD83C\uDDF2\uD83C\uDDFE',
'ca': '\uD83C\uDDEA\uD83C\uDDF8'
};
// Twemoji парсинг (SVG с CDN)
const twemojiOpts = {
folder: 'svg',
ext: '.svg'
};
function parseEmoji(el) {
if (typeof twemoji !== 'undefined') twemoji.parse(el, twemojiOpts);
}
// --- Кастомные выпадающие списки ---
function initCalcDropdown(wrapperId, inputId, flagId, nameId, includeRussian) {
const wrapper = document.getElementById(wrapperId);
if (!wrapper) return;
const btn = wrapper.querySelector('.calc-dropdown-btn');
const panel = wrapper.querySelector('.calc-dropdown-panel');
const search = wrapper.querySelector('.calc-dropdown-search');
const list = wrapper.querySelector('.calc-dropdown-list');
const hiddenInput = document.getElementById(inputId);
const flagEl = document.getElementById(flagId);
const nameEl = document.getElementById(nameId);
// Собираем языки и сортируем по алфавиту
// ru из prices.languages фильтруем — иначе «Русский» дублируется (добавляется только вручную через includeRussian)
let langs = [];
if (includeRussian) {
langs.push({ id: 'ru', flag: '🇷🇺', name: calcI18n.langNames['ru'] || 'Русский' });
}
pricesData.languages.forEach(l => {
if (l.id === 'ru') return;
langs.push({ id: l.id, flag: l.flag, name: calcI18n.langNames[l.id] || l.name });
});
langs.sort((a, b) => a.name.localeCompare(b.name));
function renderOptions(filter) {
list.innerHTML = '';
const q = (filter || '').toLowerCase();
langs.forEach(l => {
if (q && !l.name.toLowerCase().includes(q) && !l.id.includes(q)) return;
const div = document.createElement('div');
div.className = 'calc-dropdown-option' + (l.id === hiddenInput.value ? ' active' : '');
div.dataset.value = l.id;
div.innerHTML = '
' + l.flag + ' ' + l.name + ' ';
div.addEventListener('click', () => selectLang(l));
list.appendChild(div);
});
// Парсим эмодзи через Twemoji
parseEmoji(list);
}
function selectLang(l) {
hiddenInput.value = l.id;
flagEl.innerHTML = l.flag;
nameEl.textContent = l.name;
parseEmoji(flagEl);
panel.classList.remove('open');
btn.setAttribute('aria-expanded', 'false');
search.value = '';
// Событие change
hiddenInput.dispatchEvent(new Event('change'));
}
btn.addEventListener('click', (e) => {
e.stopPropagation();
// Закрыть другие
document.querySelectorAll('.calc-dropdown').forEach(w => {
if (w !== wrapper) {
w.querySelector('.calc-dropdown-panel').classList.remove('open');
w.querySelector('.calc-dropdown-btn').setAttribute('aria-expanded', 'false');
}
});
panel.classList.toggle('open');
const isOpen = panel.classList.contains('open');
btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
if (isOpen) {
renderOptions('');
setTimeout(() => search.focus(), 50);
}
});
search.addEventListener('input', () => renderOptions(search.value));
search.addEventListener('click', (e) => e.stopPropagation());
// Начальный парсинг флагов в кнопках
parseEmoji(flagEl);
}
// Закрытие при клике вне
document.addEventListener('click', () => {
document.querySelectorAll('.calc-dropdown-panel.open').forEach(p => p.classList.remove('open'));
document.querySelectorAll('.calc-dropdown-btn[aria-expanded="true"]').forEach(b => b.setAttribute('aria-expanded', 'false'));
});
// Функция расчета стоимости
function calculatePrice() {
const fromLang = document.getElementById('calc-from-lang').value;
const toLang = document.getElementById('calc-to-lang').value;
const volume = Math.max(1, parseInt(document.getElementById('calc-volume').value, 10) || 1);
// Одинаковые языки (например, ru → ru) — расчёт невозможен
if (fromLang === toLang) {
document.getElementById('calc-result').textContent = '—';
document.getElementById('calc-lang-pair').textContent =
(calcI18n.langNames[fromLang] || fromLang) + ' → ' + (calcI18n.langNames[toLang] || toLang);
document.getElementById('breakdown-translation-price').textContent = '—';
document.getElementById('breakdown-translation-label').textContent = '';
document.getElementById('breakdown-addons-container').innerHTML = '';
document.getElementById('breakdown-discount').classList.add('hidden');
document.getElementById('breakdown-total').textContent = '—';
return;
}
// Определяем направление и язык
const isToRussian = (toLang === 'ru');
const langId = isToRussian ? fromLang : toLang;
const language = pricesData.languages.find(l => l.id === langId);
if (!language) {
document.getElementById('calc-result').textContent = '—';
return;
}
// Цена за страницу зависит от направления
let pricePerPage = isToRussian ? language.price_from_foreign : language.price_to_foreign;
// Базовая стоимость перевода
let translationPrice = pricePerPage * volume;
// Проверяем скидку за объем
// break на первом совпадении: на границе (80/100/150 стр.) скидочные диапазоны
// в prices.json пересекаются; берём нижний диапазон, чтобы совпадать с таблицей цен.
let discountPercent = 0;
if (pricesData.discounts) {
for (const d of pricesData.discounts) {
if (volume >= d.from_pages && (d.to_pages === null || volume <= d.to_pages)) {
discountPercent = d.percent;
break;
}
}
}
let discountAmount = 0;
if (discountPercent > 0) {
discountAmount = Math.round(translationPrice * discountPercent / 100);
}
let translationAfterDiscount = translationPrice - discountAmount;
// Дополнительные услуги (с учётом количества документов)
let addonsTotal = 0;
const addonDetails = [];
const addons = document.querySelectorAll('.calc-addon:checked');
addons.forEach(addon => {
let pricePerDoc = parseInt(addon.dataset.price);
const id = addon.dataset.id;
const service = pricesData.additional_services.find(s => s.id === id);
const qtyInput = document.querySelector('.calc-addon-qty-input[data-id="' + id + '"]');
const qty = qtyInput ? (parseInt(qtyInput.value) || 1) : 1;
// Нотариальное: скидка от 8 документов (1000₽ вместо 1200₽)
if (id === 'notarial' && pricesData.notarial_bulk_discount && qty >= pricesData.notarial_bulk_discount.from_documents) {
pricePerDoc = pricesData.notarial_bulk_discount.price_per_document;
}
if (service && pricePerDoc > 0) {
const subtotal = pricePerDoc * qty;
addonsTotal += subtotal;
const label = (calcI18n.addons[id] || service.name) + (qty > 1 ? ' × ' + qty + ' ' + calcI18n.docs : '');
addonDetails.push({ name: label, price: subtotal, pricePerDoc: pricePerDoc, qty: qty });
// Подитог рядом с инпутом количества
const subtotalEl = document.querySelector('.calc-addon-subtotal[data-id="' + id + '"]');
if (subtotalEl) subtotalEl.textContent = qty > 1 ? '= ' + subtotal.toLocaleString('ru-RU') + ' ₽' : '';
// Обновить цену за единицу при скидке нотариального
const row = addon.closest('.calc-addon-row');
if (row) {
const unitPriceEl = row.querySelector('.addon-unit-price');
if (unitPriceEl) unitPriceEl.textContent = pricePerDoc.toLocaleString('ru-RU') + ' ₽';
}
}
});
let totalPrice = translationAfterDiscount + addonsTotal;
// Минимальный заказ
if (totalPrice < minOrder && totalPrice > 0) {
totalPrice = minOrder;
}
// Обновляем отображение
const fromName = calcI18n.langNames[fromLang] || fromLang;
const toName = calcI18n.langNames[toLang] || toLang;
document.getElementById('calc-result').textContent = totalPrice.toLocaleString('ru-RU') + ' ₽';
document.getElementById('calc-lang-pair').textContent = fromName + ' → ' + toName;
// Детализация
document.getElementById('breakdown-translation-price').textContent = translationPrice.toLocaleString('ru-RU') + ' ₽';
document.getElementById('breakdown-translation-label').textContent =
volume + ' ' + calcI18n.pages + ' ' + calcI18n.times + ' ' + pricePerPage.toLocaleString('ru-RU') + ' ₽';
// Скидка
const discountEl = document.getElementById('breakdown-discount');
if (discountPercent > 0) {
discountEl.classList.remove('hidden');
document.getElementById('breakdown-discount-label').textContent =
calcI18n.discount + ' (' + discountPercent + '%)';
document.getElementById('breakdown-discount-price').textContent =
'-' + discountAmount.toLocaleString('ru-RU') + ' ₽';
} else {
discountEl.classList.add('hidden');
}
// Допуслуги
const addonsContainer = document.getElementById('breakdown-addons-container');
addonsContainer.innerHTML = '';
addonDetails.forEach(a => {
const div = document.createElement('div');
div.className = 'flex justify-between';
div.innerHTML = '
' + calcEscapeHtml(a.name) + ' ' +
a.price.toLocaleString('ru-RU') + ' ₽ ';
addonsContainer.appendChild(div);
});
// Итого
document.getElementById('breakdown-total').textContent = totalPrice.toLocaleString('ru-RU') + ' ₽';
}
// Собрать детализацию цен для отправки на сервер
function calcGetPriceBreakdown() {
if (!pricesData) return null;
const fromLang = document.getElementById('calc-from-lang').value;
const toLang = document.getElementById('calc-to-lang').value;
const volume = Math.max(1, parseInt(document.getElementById('calc-volume').value, 10) || 1);
if (fromLang === toLang) return null;
const isToRussian = toLang === 'ru';
const language = pricesData.languages.find(l => l.id === (isToRussian ? fromLang : toLang));
if (!language) return null;
const pricePerPage = isToRussian ? language.price_from_foreign : language.price_to_foreign;
const translationPrice = pricePerPage * volume;
// break на первом совпадении (границы 80/100/150 пересекаются — см. calculatePrice).
let discountPercent = 0;
if (pricesData.discounts) {
for (const d of pricesData.discounts) {
if (volume >= d.from_pages && (d.to_pages === null || volume <= d.to_pages)) {
discountPercent = d.percent;
break;
}
}
}
const discountAmount = discountPercent > 0 ? Math.round(translationPrice * discountPercent / 100) : 0;
const items = [];
items.push({
name: volume + ' стр. × ' + pricePerPage.toLocaleString('ru-RU') + ' ₽ — перевод',
price: translationPrice
});
if (discountAmount > 0) {
items.push({
name: 'Скидка за объём (' + discountPercent + '%)',
price: -discountAmount
});
}
document.querySelectorAll('.calc-addon:checked').forEach(addon => {
let pricePerDoc = parseInt(addon.dataset.price);
const id = addon.dataset.id;
const service = pricesData.additional_services.find(s => s.id === id);
const qtyInput = document.querySelector('.calc-addon-qty-input[data-id="' + id + '"]');
const qty = qtyInput ? (parseInt(qtyInput.value) || 1) : 1;
if (id === 'notarial' && pricesData.notarial_bulk_discount && qty >= pricesData.notarial_bulk_discount.from_documents) {
pricePerDoc = pricesData.notarial_bulk_discount.price_per_document;
}
if (service && pricePerDoc > 0) {
const baseName = calcI18n.addons[id] || service.name;
const label = qty > 1
? baseName + ' × ' + qty + ' док. (' + pricePerDoc.toLocaleString('ru-RU') + ' ₽/док.)'
: baseName + ' (' + pricePerDoc.toLocaleString('ru-RU') + ' ₽)';
items.push({ name: label, price: pricePerDoc * qty });
}
});
return { items: items };
}
// === ЛОГИКА ПРИКРЕПЛЕНИЯ ФАЙЛОВ ===
const calcAttachedFiles = [];
const CALC_MAX_FILE_SIZE_MB = 50;
const CALC_MAX_FILE_SIZE = CALC_MAX_FILE_SIZE_MB * 1024 * 1024;
const CALC_ALLOWED_EXT = ['pdf','doc','docx','xls','xlsx','txt','jpg','jpeg','png','ppt','pptx','rtf','odt','zip','rar'];
const CALC_MAX_FILES = 10;
function calcFormatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function calcEscapeHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML.replace(/"/g, '"');
}
function calcRenderFiles() {
const list = document.getElementById('calc-files-list');
const btn = document.getElementById('calc-attach-btn');
if (calcAttachedFiles.length === 0) {
list.classList.add('hidden');
list.innerHTML = '';
btn.classList.remove('has-files');
return;
}
btn.classList.add('has-files');
list.classList.remove('hidden');
const totalSize = calcAttachedFiles.reduce((sum, f) => sum + f.size, 0);
list.innerHTML = calcAttachedFiles.map((f, i) => {
const safeName = calcEscapeHtml(f.name);
return '
' +
'
' +
'
' + safeName + ' ' +
'
' + calcFormatFileSize(f.size) + ' ' +
'
' +
' ' +
' ' +
'
';
}).join('');
if (calcAttachedFiles.length > 1) {
list.innerHTML += '
' + calcFormatFileSize(totalSize) + '
';
}
}
function calcRemoveFile(index) {
calcAttachedFiles.splice(index, 1);
calcRenderFiles();
}
// === МОДАЛЬНОЕ ОКНО ===
function calcOpenModal() {
const fromLang = document.getElementById('calc-from-lang').value;
const toLang = document.getElementById('calc-to-lang').value;
const volume = Math.max(1, parseInt(document.getElementById('calc-volume').value, 10) || 1);
const fromName = calcI18n.langNames[fromLang] || fromLang;
const toName = calcI18n.langNames[toLang] || toLang;
const price = document.getElementById('calc-result').textContent;
document.getElementById('modal-lang-pair').textContent = fromName + ' \u2192 ' + toName;
document.getElementById('modal-volume').textContent = volume + ' ' + calcI18n.pages;
document.getElementById('modal-price').textContent = price;
// \u0424\u0430\u0439\u043b \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u0435\u043d: \u0435\u0441\u043b\u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u043d\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d \u2014 \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u043c \u043f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0443.
document.getElementById('modal-no-file-note').classList.toggle('hidden', calcAttachedFiles.length > 0);
// Сброс ошибок
document.querySelectorAll('.calc-modal-body input, .calc-modal-body textarea').forEach(el => {
el.classList.remove('field-error');
});
document.querySelectorAll('.field-error-msg').forEach(el => el.classList.remove('visible'));
const captchaBox = document.getElementById('calc-captcha-box');
captchaBox.classList.remove('checked', 'error');
captchaBox.querySelector('svg').classList.add('hidden');
document.getElementById('calc-modal-overlay').classList.add('open');
document.body.style.overflow = 'hidden';
setTimeout(() => document.getElementById('calc-modal-name').focus(), 100);
}
function calcCloseModal() {
document.getElementById('calc-modal-overlay').classList.remove('open');
document.body.style.overflow = '';
}
// Предупреждение об отклонённых сервером вложениях. Заказ при этом сохранён —
// показываем внутри окна успеха, а не как ошибку.
function calcShowRejectedFiles(rejected) {
const box = document.getElementById('calc-success-rejected');
if (!box) return;
box.textContent = '';
if (!rejected || !rejected.length) {
box.classList.remove('open');
return;
}
const title = document.createElement('strong');
title.textContent = calcI18nUI.rejectedTitle.replace('%d', rejected.length);
box.appendChild(title);
const list = document.createElement('ul');
rejected.forEach(function (rf) {
const li = document.createElement('li');
// textContent — имя файла приходит от пользователя, не вставлять как HTML
li.textContent = (rf && rf.name ? rf.name : '—') + ' — ' + (rf && rf.reason ? rf.reason : '');
list.appendChild(li);
});
box.appendChild(list);
const cta = document.createElement('div');
cta.className = 'calc-rejected-cta';
cta.textContent = calcI18nUI.rejectedCta.replace('%s', box.dataset.email || '');
box.appendChild(cta);
box.classList.add('open');
}
// === ВАЛИДАЦИЯ ===
function calcValidateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function calcValidatePhone(phone) {
var cleaned = phone.replace(/\D/g, '');
// На EN/ZH-версиях допускаем международные номера: достаточно ≥10 цифр
if (!(document.documentElement.lang || '').toLowerCase().startsWith('ru')) {
return cleaned.length >= 10;
}
return cleaned.length === 11 && cleaned[0] === '7';
}
function calcValidateModal() {
let valid = true;
// Имя
const name = document.getElementById('calc-modal-name');
const nameErr = document.getElementById('calc-modal-name-error');
if (!name.value.trim()) {
name.classList.add('field-error');
nameErr.classList.add('visible');
valid = false;
} else {
name.classList.remove('field-error');
nameErr.classList.remove('visible');
}
// Телефон
const phone = document.getElementById('calc-modal-phone');
const phoneErr = document.getElementById('calc-modal-phone-error');
if (!phone.value.trim() || !calcValidatePhone(phone.value)) {
phone.classList.add('field-error');
phoneErr.classList.add('visible');
phoneErr.textContent = '!';
valid = false;
} else {
phone.classList.remove('field-error');
phoneErr.classList.remove('visible');
}
// Email
const email = document.getElementById('calc-modal-email');
const emailErr = document.getElementById('calc-modal-email-error');
if (!email.value.trim() || !calcValidateEmail(email.value)) {
email.classList.add('field-error');
emailErr.classList.add('visible');
emailErr.textContent = '!';
valid = false;
} else {
email.classList.remove('field-error');
emailErr.classList.remove('visible');
}
// Капча
const captchaBox = document.getElementById('calc-captcha-box');
const captchaErr = document.getElementById('calc-captcha-error');
if (!captchaBox.classList.contains('checked')) {
captchaBox.classList.add('error');
captchaErr.classList.add('visible');
valid = false;
} else {
captchaBox.classList.remove('error');
captchaErr.classList.remove('visible');
}
return valid;
}
// === ОТПРАВКА ЗАЯВКИ ===
async function calcSubmitOrder() {
if (!calcValidateModal()) return;
const submitBtn = document.getElementById('calc-modal-submit');
const submitText = document.getElementById('calc-modal-submit-text');
const spinner = document.getElementById('calc-modal-spinner');
submitBtn.disabled = true;
submitText.textContent = calcI18nUI.sending;
spinner.classList.remove('hidden');
try {
const formData = new FormData();
formData.append('source', 'calculator');
if (window.UCCsrf) formData.append('csrf', window.UCCsrf.token());
formData.append('name', document.getElementById('calc-modal-name').value.trim());
formData.append('phone', document.getElementById('calc-modal-phone').value.trim());
formData.append('email', document.getElementById('calc-modal-email').value.trim());
formData.append('message', document.getElementById('calc-modal-comment').value.trim());
formData.append('lang_from', calcI18n.langNames[document.getElementById('calc-from-lang').value] || document.getElementById('calc-from-lang').value);
formData.append('lang_to', calcI18n.langNames[document.getElementById('calc-to-lang').value] || document.getElementById('calc-to-lang').value);
formData.append('volume', document.getElementById('calc-volume').value);
formData.append('estimated_price', document.getElementById('calc-result').textContent);
// Информация о клиенте (браузер, экран, язык, таймзона)
formData.append('user_agent', navigator.userAgent);
formData.append('screen_resolution', screen.width + '×' + screen.height);
formData.append('viewport', window.innerWidth + '×' + window.innerHeight);
formData.append('browser_lang', navigator.language || navigator.userLanguage || '');
formData.append('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone || '');
formData.append('referrer', document.referrer || '');
formData.append('page_url', window.location.href);
try { formData.append('lead_attribution', JSON.stringify(window.getLeadAttribution ? window.getLeadAttribution() : {})); } catch(e){}
// Допуслуги (с количеством)
const addonNames = [];
document.querySelectorAll('.calc-addon:checked').forEach(cb => {
const id = cb.dataset.id;
const qtyInput = document.querySelector('.calc-addon-qty-input[data-id="' + id + '"]');
const qty = qtyInput ? (parseInt(qtyInput.value) || 1) : 1;
const label = calcI18n.addons[id] || id;
addonNames.push(qty > 1 ? label + ' × ' + qty : label);
});
formData.append('addons', addonNames.join(', '));
// Детализация цен для письма менеджеру
const breakdown = calcGetPriceBreakdown();
if (breakdown) {
formData.append('price_breakdown', JSON.stringify(breakdown));
}
// Файлы
calcAttachedFiles.forEach((file, i) => {
formData.append('files[]', file);
});
// Сколько файлов клиент реально приложил. Если PHP примет меньше
// (max_file_uploads), сервер сообщит о разнице в rejected_files.
formData.append('expected_files_count', String(calcAttachedFiles.length));
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 120000);
console.log('[calculator] Sending request...');
const response = await fetch('/api/calculator-order.php', {
method: 'POST',
body: formData,
signal: controller.signal
});
clearTimeout(timeoutId);
console.log('[calculator] Response status:', response.status);
const data = await response.json();
if (data.success) {
calcCloseModal();
// Показать окно успеха
document.getElementById('calc-success-overlay').classList.add('open');
document.body.style.overflow = 'hidden';
// Часть файлов могла быть отклонена (формат/размер/сбой загрузки).
// Заказ сохранён, но клиент должен знать, что вложение не дошло.
calcShowRejectedFiles(data.rejected_files);
// Сброс формы
document.getElementById('calc-modal-name').value = '';
document.getElementById('calc-modal-phone').value = '';
document.getElementById('calc-modal-email').value = '';
document.getElementById('calc-modal-comment').value = '';
calcAttachedFiles.length = 0;
calcRenderFiles();
// Аналитика
if (typeof ym !== 'undefined' && window.YANDEX_METRIKA_ID) {
ym(window.YANDEX_METRIKA_ID, 'reachGoal', 'calculator_order');
}
if (typeof gtag !== 'undefined') {
gtag('event', 'submit', { event_category: 'Calculator', event_label: 'Order' });
}
} else {
alert(data.message || calcI18nUI.errorGeneric);
}
} catch (err) {
console.error('Calculator submit error:', err);
if (err.name === 'AbortError') {
alert(calcI18nUI.errorNetwork);
} else {
alert(calcI18nUI.errorGeneric);
}
} finally {
submitBtn.disabled = false;
submitText.textContent = calcI18nUI.submitText;
spinner.classList.add('hidden');
}
}
function calcCloseSuccess() {
document.getElementById('calc-success-overlay').classList.remove('open');
document.body.style.overflow = '';
}
// Обработчики изменений
document.addEventListener('DOMContentLoaded', () => {
// Инициализация кастомных дропдаунов (ru добавляется вручную, в списках prices.languages он отфильтрован)
initCalcDropdown('from-lang-dropdown', 'calc-from-lang', 'from-lang-flag', 'from-lang-name', true);
initCalcDropdown('to-lang-dropdown', 'calc-to-lang', 'to-lang-flag', 'to-lang-name', true);
['calc-from-lang', 'calc-to-lang', 'calc-volume'].forEach(id => {
const el = document.getElementById(id);
if (el) {
el.addEventListener('change', calculatePrice);
el.addEventListener('input', calculatePrice);
}
});
document.querySelectorAll('.calc-addon').forEach(cb => {
cb.addEventListener('change', function() {
const id = this.dataset.id;
const qtyDiv = this.closest('.calc-addon-row').querySelector('.calc-addon-qty');
if (qtyDiv) {
if (this.checked) {
qtyDiv.classList.remove('hidden');
} else {
qtyDiv.classList.add('hidden');
// Сброс количества при отключении
const qtyInput = qtyDiv.querySelector('.calc-addon-qty-input');
if (qtyInput) qtyInput.value = 1;
const subtotalEl = qtyDiv.querySelector('.calc-addon-subtotal');
if (subtotalEl) subtotalEl.textContent = '';
}
}
calculatePrice();
});
});
// Изменение количества документов → пересчёт
document.querySelectorAll('.calc-addon-qty-input').forEach(input => {
input.addEventListener('input', calculatePrice);
});
// Синхронизация: один из языков должен быть русским
document.getElementById('calc-from-lang').addEventListener('change', function() {
if (this.value !== 'ru' && document.getElementById('calc-to-lang').value !== 'ru') {
document.getElementById('calc-to-lang').value = 'ru';
document.getElementById('to-lang-flag').innerHTML = '🇷🇺';
document.getElementById('to-lang-name').textContent = calcI18n.langNames['ru'];
parseEmoji(document.getElementById('to-lang-flag'));
}
calculatePrice();
});
document.getElementById('calc-to-lang').addEventListener('change', function() {
if (this.value !== 'ru' && document.getElementById('calc-from-lang').value !== 'ru') {
document.getElementById('calc-from-lang').value = 'ru';
document.getElementById('from-lang-flag').innerHTML = '🇷🇺';
document.getElementById('from-lang-name').textContent = calcI18n.langNames['ru'];
parseEmoji(document.getElementById('from-lang-flag'));
}
calculatePrice();
});
calculatePrice();
// --- Файловые аттачменты ---
const attachBtn = document.getElementById('calc-attach-btn');
const fileInput = document.getElementById('calc-file-input');
attachBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
for (const file of e.target.files) {
// Лимит PHP max_file_uploads: файлы сверх него сервер молча отбрасывает,
// поэтому не даём добавить их в список вообще.
if (calcAttachedFiles.length >= CALC_MAX_FILES) {
alert(calcI18nUI.tooManyFiles.replace('%d', CALC_MAX_FILES));
break;
}
const ext = file.name.split('.').pop().toLowerCase();
if (!CALC_ALLOWED_EXT.includes(ext)) {
alert(file.name + ' — invalid format');
continue;
}
if (file.size > CALC_MAX_FILE_SIZE) {
alert(file.name + ' — max ' + CALC_MAX_FILE_SIZE_MB + ' MB');
continue;
}
calcAttachedFiles.push(file);
}
calcRenderFiles();
fileInput.value = ''; // Сброс, чтобы можно было выбрать тот же файл повторно
// Сбрасываем ошибку «файл обязателен» при добавлении файлов
if (calcAttachedFiles.length > 0) {
var attachBtn = document.getElementById('calc-attach-btn');
var fileErr = document.getElementById('calc-file-error');
attachBtn.classList.remove('error');
attachBtn.style.borderColor = '';
fileErr.classList.add('hidden');
}
});
// --- Кнопка «Отправить заявку» → открыть модал ---
// Файл НЕ обязателен: можно отправить заявку без документа (он будет прислан позже).
document.getElementById('calc-submit-btn').addEventListener('click', function() {
// Одинаковые языки — заказ не имеет смысла, расчёт показывает «—»
if (document.getElementById('calc-from-lang').value === document.getElementById('calc-to-lang').value) {
return;
}
calcOpenModal();
});
// --- Закрытие модалки ---
document.getElementById('calc-modal-close').addEventListener('click', calcCloseModal);
// --- Капча ---
const captchaBox = document.getElementById('calc-captcha-box');
captchaBox.addEventListener('click', () => {
captchaBox.classList.toggle('checked');
const svg = captchaBox.querySelector('.captcha-checkbox svg');
if (captchaBox.classList.contains('checked')) {
svg.classList.remove('hidden');
captchaBox.classList.remove('error');
document.getElementById('calc-captcha-error').classList.remove('visible');
} else {
svg.classList.add('hidden');
}
});
// --- Отправка ---
document.getElementById('calc-modal-submit').addEventListener('click', calcSubmitOrder);
// --- Закрытие окна успеха ---
document.getElementById('calc-success-close').addEventListener('click', calcCloseSuccess);
// --- Реалтайм-валидация: убрать красное при вводе ---
['calc-modal-name', 'calc-modal-phone', 'calc-modal-email'].forEach(id => {
const el = document.getElementById(id);
if (el) {
el.addEventListener('input', () => {
el.classList.remove('field-error');
const errEl = document.getElementById(id + '-error');
if (errEl) errEl.classList.remove('visible');
});
}
});
// --- Маска телефона (+7 только на русской версии; на EN/ZH свободный ввод международных номеров) ---
const phoneInput = document.getElementById('calc-modal-phone');
if ((document.documentElement.lang || '').toLowerCase().startsWith('ru')) {
phoneInput.addEventListener('input', function() {
let val = this.value.replace(/\D/g, '');
if (val.length > 0 && val[0] === '8') val = '7' + val.slice(1);
if (val.length > 11) val = val.slice(0, 11);
if (val.length === 0) { this.value = ''; return; }
let formatted = '+' + val[0];
if (val.length > 1) formatted += ' (' + val.slice(1, 4);
if (val.length > 4) formatted += ') ' + val.slice(4, 7);
if (val.length > 7) formatted += '-' + val.slice(7, 9);
if (val.length > 9) formatted += '-' + val.slice(9, 11);
this.value = formatted;
});
}
// --- Кнопка «Сформировать КП» ---
// Контекст последнего сформированного КП — для лёгкого захвата контакта (email).
let quoteLeadContext = null;
const quoteBtn = document.getElementById('calc-quote-btn');
if (quoteBtn) {
quoteBtn.addEventListener('click', function() {
const breakdown = calcGetPriceBreakdown();
if (!breakdown || !breakdown.items.length) return;
const fromLang = document.getElementById('calc-from-lang').value;
const toLang = document.getElementById('calc-to-lang').value;
const volume = document.getElementById('calc-volume').value;
const totalPrice = document.getElementById('calc-result').textContent;
const fromName = calcI18n.langNames[fromLang] || fromLang;
const toName = calcI18n.langNames[toLang] || toLang;
// Собираем допуслуги
const addonNames = [];
document.querySelectorAll('.calc-addon:checked').forEach(cb => {
const id = cb.dataset.id;
const qtyInput = document.querySelector('.calc-addon-qty-input[data-id="' + id + '"]');
const qty = qtyInput ? (parseInt(qtyInput.value) || 1) : 1;
const label = calcI18n.addons[id] || id;
addonNames.push(qty > 1 ? label + ' \u00d7 ' + qty : label);
});
// Формируем URL с параметрами
const params = new URLSearchParams({
lang: 'ru',
from: fromName,
to: toName,
volume: volume,
total: totalPrice,
breakdown: JSON.stringify(breakdown.items),
addons: addonNames.join(', ')
});
window.open('/api/generate-quote.php?' + params.toString(), '_blank');
// КП уже открыт — предлагаем (необязательно) отправить его на email.
quoteLeadContext = { fromName, toName, volume, totalPrice, addons: addonNames.join(', ') };
const leadBlock = document.getElementById('quote-lead-block');
if (leadBlock) {
leadBlock.classList.remove('hidden');
document.getElementById('quote-lead-success').classList.add('hidden');
}
});
}
// --- Отправка КП на email (лёгкий захват лида, не блокирует КП) ---
const leadSubmitBtn = document.getElementById('quote-lead-submit');
if (leadSubmitBtn) {
leadSubmitBtn.addEventListener('click', async function() {
const emailEl = document.getElementById('quote-lead-email');
const nameEl = document.getElementById('quote-lead-name');
const hp = document.getElementById('quote-lead-website');
const successEl = document.getElementById('quote-lead-success');
// Honeypot
if (hp && hp.value) return;
const email = emailEl.value.trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
emailEl.classList.add('border-red-500');
emailEl.focus();
alert(calcI18nUI.quoteLeadInvalidEmail);
return;
}
emailEl.classList.remove('border-red-500');
leadSubmitBtn.disabled = true;
leadSubmitBtn.textContent = '...';
try {
const ctx = quoteLeadContext || {};
const formData = new FormData();
formData.append('source', 'quote_lead');
// Honeypot уезжает на сервер: клиентской проверки hp.value мало — бот,
// бьющий прямо в /api/contact.php, обходил её, а серверная проверка
// (!empty($_POST['website'])) без этого поля не срабатывала никогда.
formData.append('website', hp ? hp.value : '');
if (window.UCCsrf) formData.append('csrf', window.UCCsrf.token());
formData.append('name', nameEl.value.trim());
formData.append('email', email);
formData.append('lang_from', ctx.fromName || '');
formData.append('lang_to', ctx.toName || '');
formData.append('message', [ctx.volume ? ctx.volume + ' ' + calcI18n.pages : '', ctx.totalPrice ? '— ' + ctx.totalPrice : '', ctx.addons || ''].filter(Boolean).join(' '));
formData.append('user_agent', navigator.userAgent);
formData.append('screen_resolution', screen.width + '×' + screen.height);
formData.append('viewport', window.innerWidth + '×' + window.innerHeight);
formData.append('browser_lang', navigator.language || navigator.userLanguage || '');
formData.append('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone || '');
formData.append('referrer', document.referrer || '');
formData.append('page_url', window.location.href);
try { formData.append('lead_attribution', JSON.stringify(window.getLeadAttribution ? window.getLeadAttribution() : {})); } catch(e){}
const response = await fetch('/api/contact.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.success) {
successEl.classList.remove('hidden');
emailEl.value = '';
nameEl.value = '';
if (typeof ym !== 'undefined' && window.YANDEX_METRIKA_ID) {
ym(window.YANDEX_METRIKA_ID, 'reachGoal', 'quote_lead');
}
if (typeof gtag !== 'undefined') {
gtag('event', 'submit', { event_category: 'Calculator', event_label: 'Quote Lead' });
}
} else {
alert(data.message || calcI18nUI.errorGeneric);
}
} catch (err) {
alert(calcI18nUI.errorGeneric);
} finally {
leadSubmitBtn.disabled = false;
leadSubmitBtn.textContent = calcI18nUI.quoteLeadSubmit;
}
});
}
});